uoj37 【清华集训2014】主旋律 做题心得
前言
好一个毒瘤数数题
以前集训课上,听老师讲过;但是当时完全没听懂,所以今天来补一补
思维过程
算是我做这个题的完整心路历程吧,前面的错误想法可以选择性的看,对正解影响不大
这个 (n) 非常小,只有 (15);但是这个 (m) 却非常大,最大200余。看来我们的算法需要和 (n) 有关,而且可以猜到是指数的,毕竟这个问题一看就很np
那我们考虑点。发现直接考虑,所有点都在一大块SCC里面,能考虑个寂寞。于是,容易想到反面考虑:算多少种边集使得图 不 强联通
不强联通,那可以考虑缩点,然后就会变成一堆SCC块,它们组成DAG。
发现DAG有一个性质,就是我们可以把点集划一刀,分成 (A,B) 两块,使得只存在 (A) 到 (B) 的边。
然后我们设 (f(S)) 表示 (S) 点集强联通的方案,而 (g(S)) 是不强联通。那么 (f(S)+g(S)=2^{E(S)}),其中 (E(S)) 表示 (S) 内部边数。也就是说,这俩算一个就行
看来是 (g) 比较好算。根据上面的 “划一刀” 理论,机智的想到:我们枚举它一个子集 (T),然后
(g(S)=sumlimits_{T} 2^{E(T)} imes 2^{E(S/T)} imes 2^{E(T ightarrow S/T)})
(S/T) 表示 (T) 在 (S) 中的补集,因为我不知道正规写法怎么用 (LaTeX) 打,就暂且这样写
(E(A,B)) 表示从 (A) 集合到 (B) 集合的边数,即 (# (u,v)|uin A,vin B)
一看怎么这么简单,肯定不对。拿脑子一想,肯定得重,因为我们这个“切一刀”的方法不唯一,一个缩点的方案会被算很多遍
能不能去重呢?考虑序列的去重,我们任意划一刀肯定会重,但如果每次只切掉第一个位置,就不会重
再来考虑这个问题,同样是找一个 唯一 的 “开始” 位置。容易想到,就是那些入度为 (0) 的点。
那我们如果要计算 (g) ,可以枚举一个集合 (T),强制它作为"开始"点。然后搞一搞 (T ightarrow S/T) 的边就行了
设 (G(S)) 表示,把点集 (S) 分成若干部分,使得每个部分是一个SCC,且SCC间两两没有边,方案数。
然后我们枚举 (T) 作为“开始”,内部划分方案数就是 (G(T))。然后 (T ightarrow S/T),(S/T) 内部,这些边都任选。得到式子:
这回脑子转的久一点,但是又发现重了:因为我们没有保证 (S/T) 里面没有“开始”点!
我自己想就到这,想了好久发现不知道咋整,就去听老师讲回放了
我一看,嗷,原来我的容斥已经完全不熟练了,尽管看出来要容斥,也不知道咋搞系数了
假设最终缩成的DAG上有 (k) 个“开始”点,那么它的 (2^k-1) 个非空子集都会被我们枚举,并且把它算一次。也就是说,它一共会被算 (2^k-1) 次。
关于子集的去重,大家应该都很熟悉。假设 (T) 里面分了 (k) 块 SCC,乘一个 ((-1)^{k+1}) 作为容斥系数就行了。
形象的说,就是:分一块方案数,-分两块,+分三块,-分四块...
为啥:
假设最后是 (k) 块,考虑它被算了几次:
对于 (T) 里面分了 (k') 块的时候,会把这个东西算 (inom{k}{k'}) 次。那一共被算:
(sumlimits_{k'=1}^{k} inom{k}{k'} imes (-1)^{k'+1})
我们发现这玩意就是二项式定理的形式,瞎几把搞一搞,最后发现它就是 (1)。
那现在似乎式子对了,设 (G_k(T)) 表示把 (T) 分成 (k) 块 SCC,SCC间没有边的方案数。则:
我们发现我们并不需要知道具体的 (G) 值,只需要分奇偶就行了。那我们给它换个定义:(G_{0/1}(T)) 表示把 (T) 分成偶数/奇数块SCC,SCC间没有边的方案数。则:
然后我们就能得到 (f),然后有了 (f) 之后,(G_0,G_1) 也可以交叉着互推
然后注意一些边界问题,就ok了。
代码实现啥的并不是本题的重点,直接参考代码即可
总结
-
容斥的套路:奇数+,偶数-,解决子集算重的情况
-
脑子里先搞一个大概想法,然后再往这个想法上面凑
比如这个题,就可以先看出来是个容斥,然后想想咋容斥
代码
#include <bits/stdc++.h>
using namespace std;
namespace Flandre_Scarlet
{
#define N 15
#define int long long
#define mod 1000000007
#define i2 500000004
#define F(i,l,r) for(int i=l;i<=r;++i)
#define D(i,r,l) for(int i=r;i>=l;--i)
#define Fs(i,l,r,c) for(int i=l;i<=r;c)
#define Ds(i,r,l,c) for(int i=r;i>=l;c)
#define MEM(x,a) memset(x,a,sizeof(x))
#define FK(x) MEM(x,0)
#define Tra(i,u) for(int i=G.st(u),v=G.to(i);~i;i=G.nx(i),v=G.to(i))
#define p_b push_back
#define sz(a) ((int)a.size())
#define all(a) a.begin(),a.end()
#define iter(a,p) (a.begin()+p)
int I() {char c=getchar(); int x=0; int f=1; while(c<'0' or c>'9') f=(c=='-')?-1:1,c=getchar(); while(c>='0' and c<='9') x=(x<<1)+(x<<3)+(c^48),c=getchar(); return ((f==1)?x:-x);}
template <typename T> void Rd(T& arg){arg=I();}
template <typename T,typename...Types> void Rd(T& arg,Types&...args){arg=I(); Rd(args...);}
void RA(int *p,int n) {F(i,1,n) *p=I(),++p;}
int n,m,U;
bool e[N][N];
bitset<300> so[1<<N],si[1<<N]; // 搞两个bitset, 快速支持E(A,B)的查询
void Input()
{
n=I(),m=I(); U=(1<<n)-1;
F(i,1,m)
{
int u=I()-1,v=I()-1;
F(s,1,U)
{
if ((s>>u)&1) so[s][i]=1;
else so[s][i]=0;
if ((s>>v)&1) si[s][i]=1;
else si[s][i]=0;
}
}
}
int lg[1<<N],pw2[300];
void init()
{
pw2[0]=1;
F(i,1,250) pw2[i]=pw2[i-1]*2%mod;
F(i,0,n-1) lg[1<<i]=i;
F(i,1,U) if (!lg[i]) lg[i]=lg[i-1];
}
int E(int S,int T)
{
int ans=(so[S]&si[T]).count();
return ans;
}
int f[1<<N],g[1<<N],G[2][1<<N];
void Sakuya()
{
init();
g[0]=0;
G[0][0]=1; G[1][0]=0;
F(s,1,U)
{
for(int t=(s-1)&s;t;t=(t-1)&s)
{
g[s]+=pw2[E(t,s^t)+E(s^t,s^t)]*(G[1][t]-G[0][t]%mod+mod)%mod;
g[s]%=mod;
if (t&(s&(-s)))
{
G[0][s]+=f[t]*G[1][s^t]%mod;
G[1][s]+=f[t]*G[0][s^t]%mod;
G[0][s]%=mod; G[1][s]%=mod;
}
}
g[s]=(g[s]+G[1][s]+mod-G[0][s]%mod)%mod;
f[s]=(pw2[E(s,s)]-g[s]%mod+mod)%mod;
G[1][s]=(G[1][s]+f[s])%mod;
}
printf("%lld
",f[U]);
}
void IsMyWife()
{
Input();
Sakuya();
}
}
#undef int //long long
int main()
{
Flandre_Scarlet::IsMyWife();
getchar();
return 0;
}