uoj37 【清华集训2014】主旋律 做题心得
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)=\sum\limits_{T} 2^{E(T)}\times 2^{E(S/T)}\times 2^{E(T\rightarrow S/T)}\)
\(S/T\) 表示 \(T\) 在 \(S\) 中的补集,因为我不知道正规写法怎么用 \(\LaTeX\) 打,就暂且这样写
\(E(A,B)\) 表示从 \(A\) 集合到 \(B\) 集合的边数,即 \(\# (u,v)|u\in A,v\in B\)
一看怎么这么简单,肯定不对。拿脑子一想,肯定得重,因为我们这个“切一刀”的方法不唯一,一个缩点的方案会被算很多遍
能不能去重呢?考虑序列的去重,我们任意划一刀肯定会重,但如果每次只切掉第一个位置,就不会重
再来考虑这个问题,同样是找一个 唯一 的 “开始” 位置。容易想到,就是那些入度为 \(0\) 的点。
那我们如果要计算 \(g\) ,可以枚举一个集合 \(T\),强制它作为"开始"点。然后搞一搞 \(T\rightarrow S/T\) 的边就行了
设 \(G(S)\) 表示,把点集 \(S\) 分成若干部分,使得每个部分是一个SCC,且SCC间两两没有边,方案数。
然后我们枚举 \(T\) 作为“开始”,内部划分方案数就是 \(G(T)\)。然后 \(T\rightarrow S/T\),\(S/T\) 内部,这些边都任选。得到式子:
这回脑子转的久一点,但是又发现重了:因为我们没有保证 \(S/T\) 里面没有“开始”点!
我自己想就到这,想了好久发现不知道咋整,就去听老师讲回放了
我一看,嗷,原来我的容斥已经完全不熟练了,尽管看出来要容斥,也不知道咋搞系数了
假设最终缩成的DAG上有 \(k\) 个“开始”点,那么它的 \(2^k-1\) 个非空子集都会被我们枚举,并且把它算一次。也就是说,它一共会被算 \(2^k-1\) 次。
关于子集的去重,大家应该都很熟悉。假设 \(T\) 里面分了 \(k\) 块 SCC,乘一个 \((-1)^{k+1}\) 作为容斥系数就行了。
形象的说,就是:分一块方案数,-分两块,+分三块,-分四块...
为啥:
假设最后是 \(k\) 块,考虑它被算了几次:
对于 \(T\) 里面分了 \(k'\) 块的时候,会把这个东西算 \(\binom{k}{k'}\) 次。那一共被算:
\(\sum\limits_{k'=1}^{k} \binom{k}{k'}\times (-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\n",f[U]);
}
void IsMyWife()
{
Input();
Sakuya();
}
}
#undef int //long long
int main()
{
Flandre_Scarlet::IsMyWife();
getchar();
return 0;
}