BZOJ 4671 异或图
Description
给定 \(s\) 个图,\(G1\) 与 \(G2\) 的异或定义为:若边 \((a,b)\) 在 \(G1\) 与 \(G2\) 中出现的次数只和为 \(1\) ,则在新图 \(G\) 中存在该边,反之则不存在。求 \(S=\{G1,G2,G3,...,Gs\}\) 有多少个子集的异或为一个连通图
\(s\le 60\),节点数 \(\le 10\)
Solution
听说这种容斥叫做斯特林数形式的容斥,因为一个具有 \(n\) 种性质的元素在计算至少有 \(i\) 种性质的元素时会被计算到 \(\begin{Bmatrix}n\\i\end{Bmatrix}\) 次而不是我们常见的 \(\dbinom{n}{i}\) 次
发现直接暴力枚举哪几个点属于一个连通块算起来情况比较多不好处理
考虑容斥
设 \(A_i\) 为 \(n\) 个点至少被划分为 \(i\) 个连通块时的方案集合
因为对于一个具有 \(n\) 个连通块的划分方法,我们在枚举至少有 \(i\) 个连通块时必定会将其算到 \(S(n,i)\) 次,所以我们想求得一个容斥系数 \(f_i\) ,使得
斯特林反演一下,有
即
有了容斥系数之后我们就只需考虑对于一种将元素划分为 \(k\) 个集合方案如何计算它至少被分成 \(k\) 个联通块的方案数
我们学习题目中的方法,将 \(n\) 个点之间的连边情况用 \(\frac{n(n-1)}{2}\) 位二进制数来表示
如果两个点不属于同一个集合,则该位必须为 \(1\) ,反之则 \(0\) 或 \(1\) 皆可
换句话来说就是如果我们把强制不能连边的位赋为 \(0\) ,其余赋成 \(1\) ,得到一个数 \(c\) ,那么我们需要求的就是 \(s\) 张图异或出 \(c\) 的子集的方案数,其中 \(x\) 为数 \(c\) 的子集定义为:\(x|c=c\)
这个怎么算呢?线性基?但是我们并不能快速知道数 \(c\) 的子集中有多少数可以被异或出来
其实并没有这么麻烦,既然有些位 \(0,1\) 都能选,我们就删去这些位,只留下必须为 \(0\) 的位,这样我们就只需要求这些数异或出 \(0\) 的方案数,这个就很好算了,就是 \(2^{|S|-|T|}-1\),其中 \(|S|=s\),\(|T|\) 为线性基的大小。当然如果你认为什么都不选也是一种方案的话,那么 \(2^{|S|-|T|}\) 也可以带进去算,这样也是对的,因为合法方案一定不会是什么都不选,所以最后这些非法方案一定会被容斥掉
然后这题就做完了,复杂度 \(O(B_nsn^2)\) ,其中 \(B_n\) 为贝尔数。实际上是达不到这个上界的
代码如下:
#include<cstdio>
#include<iostream>
#include<cstring>
#include<cmath>
using namespace std;
typedef long long ll;
const int N=1e2+10;
const int M=70;
int S,nn,n,c,p[N],a[N],b[N],suc;char s[N];bool g[M][M][M];ll ans,bas[N],bin[N],fac[N];
inline void Preprocess(){
bin[0]=1;for(register int i=1;i<=60;i++)bin[i]=2ll*bin[i-1];
fac[0]=1;for(register int i=1;i<=10;i++)fac[i]=1ll*fac[i-1]*i;
}
inline void Insert(ll x){
for(register int i=50;i>=0;i--)
if((x>>i)&1){
if(bas[i])x^=bas[i];
else{bas[i]=x;suc++;break;}
}
}
inline void calc(int num){
ll val=0;int cc=0;suc=0;
for(register int i=1;i<=n;i++)
for(register int j=i+1;j<=n;j++)
if(p[i]!=p[j])a[++cc]=i,b[cc]=j;
memset(bas,0,sizeof(bas));
for(register int w=1;w<=S;w++){
val=0;
for(register int i=1;i<=cc;i++)
val|=bin[i]*g[w][a[i]][b[i]];
Insert(val);
}
ans+=(num&1? 1:-1)*fac[num-1]*(bin[S-suc]-1);
}
inline void DFS(int u,int num){
if(u==n+1){calc(num);return;}
for(register int i=1;i<=num+1;i++)p[u]=i,DFS(u+1,num+(i>num));
}
int main(){
scanf("%d",&S);Preprocess();
for(register int i=1;i<=S;i++){
scanf("%s",s+1);
if(i==1)nn=strlen(s+1),n=((int)sqrt(8*nn+1)+1)>>1;c=0;
for(register int j=1;j<=n;j++)
for(register int k=j+1;k<=n;k++)
g[i][j][k]=s[++c]-'0';
}
DFS(1,0);
printf("%lld\n",ans);
return 0;
}