入门图容斥计数
入门级无向图容斥计数
ABC213G connectivity 2
给定一个连通图,问你对于每个点 \(k\) 有多少删去 \(0\) 条边及以上的新图满足 \(1\) 和 \(k\) 连通,\(n \le 17\)。
令 \(f(S)\) 表示令 \(S\) 这个点集连通的方案数,那么一个包含了 \(1\) 和 \(k\) 的 \(S\),其 \(f(S)\) 就可以对 \(ans_k\) 作出贡献。
直接计算 \(f(S)\) 无疑是困难的,考虑容斥。令 \(g(S)\) 表示点集 \(S\) 能形成的图的数量,这个很好算,就等于 \(2^{cnt_S}\),其中 \(cnt_S\) 为 \(S\) 内部的边的条数。
我们说列式子就是从两个不同的角度描述同一个东西,再令他们相等。前面我们已经用 \(2^{cnt_S}\) 来描述了 \(g(S)\),现在我们考虑再用其他角度,倘若固定了 \(S\) 中的一个点 \(v\) 的话,令 \(v \in T\),则 \(g(S) = \sum_{v \in T \subseteq S}f(T)\times g(S/T)\),就是一部分连通,另一部分随意连的方案数。
那我们把要求的 \(f(S)\) 移项出来,得到 \(f(S)\times g(\emptyset) = g(S) - \sum_{v \in T \subset S}f(T)\times g(S/T)\),其中 \(g(\emptyset) = 1\)。
这个式子的意义是,连通的方案数等于总方案减去不合法方案。考虑分析不合法的情况,显然至少有两个连通分量,且这两个连通分量之间没有任何边,为了不重不漏,我们把 \(S\) 里的一个点 \(v\) 拿出来, 只分析包含了 \(v\) 的 \(T\)。其实是一种子集卷积。
注意最后算答案时每个 \(f(S)\) 要乘以 \(2^{m-cnt_S}\),这里的 \(cnt_S\) 表示有一段在 \(S\) 内的边数,而这些边之外的边是任意选或不选的。
时间复杂度 \(O(3^n)\),瓶颈在枚举子集。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N = 25,V=(1<<18),mod=998244353;
int u[N*N],v[N*N];
vector<int>e[N];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
int f[V],g[V],ans[N];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u[i]>>v[i];
}
for(int i=0;i<(1<<n);i++){
int cnt=0;
for(int j=1;j<=m;j++){
cnt+=(((i>>(u[j]-1))&1)&((i>>(v[j]-1))&1));
}
g[i]=qpow(2,cnt);
}
for(int s=0;s<(1<<n);s++){
int v=(s&(-s));
int mk=s^v,x=0;
f[s]=g[s];
for(int j=(mk-1)&mk;j>=0;j=(j-1)&mk){
int t=j|v;
x=(x+f[t]%mod*g[s^t]%mod)%mod;
if(!j)break;
}
f[s]=(f[s]-x+mod)%mod;
}
for(int s=0;s<(1<<n);s++){
if(!(s&1))continue;
int cntm=0;
for(int j=1;j<=m;j++){
cntm+=(((s>>(u[j]-1))&1)|((s>>(v[j]-1))&1));
}
for(int i=2;i<=n;i++){
if(!((s>>(i-1))&1))continue;
ans[i]=(ans[i]%mod+f[s]%mod*qpow(2,m-cntm)%mod)%mod;
}
}
for(int i=2;i<=n;i++)cout<<ans[i]<<endl;
}
ARC105F Lights Out on Connected Graph
这题和上题很像,要求删边后所有点连通的方案数,并追加了一个条件,转化一下就是不能有奇环,所以要求的是连通二分图的个数。
还是考虑容斥,\(f(S)\) 表示点集 \(S\) 形成连通二分图的方案数,\(g(S)\) 表示形成二分图的方案数。先考虑 \(g(S)\) 怎么算,如果是二分图,不难想到把 \(S\) 分成两个子集 \(U\) 和 \(S/U\),令 \(U\) 内为黑点,\(S/U\) 内为白点,相同子集内不能连边,就只考虑两个子集之间的连边,设 \(E(U,S/U)\) 表示这两子集之间的边数,则 \(g(S) = \sum_{U \subseteq S} 2^{E(U,S/U)}\),注意 \(U\) 可以是 \(S\) 本身。
\(E(U,S/U)\) 是好算的,我们 \(O(2^nm)\) 预处理出每个子集内部有几条边,记作 \(cnt_S\),\(E(U,S/U) = cnt_S-cnt_U-cnt_{S/U}\)。
然后还是换个角度考虑 \(g(S)\),还是从连通性的角度出发来和 \(f(S)\) 挂钩,固定一个点 \(v\),把 \(S\) 分成包含 \(v\) 的连通块和其他不在意的部分,则 \(g(S)=\sum_{v \in T \subseteq S} 2 \times f(T)\times g(S/T)\),为什么是 \(2\times f(T)\) 呢,因为我们的答案 \(f(S)\) 是不考虑左部和右部谁是黑点谁是白点的,但是我们算 \(g(S)\) 的时候却考虑了,因为前面的计算过程中同一个子集既会作为一次 \(U\) 也会作为一次 \(S/U\)。
老样子把 \(f(S)\) 移出来,则 \(2f(S) = g(S)-\sum_{v\in T \subset S}2f(T)\times g(S/T)\),我们最后输出的时候再除以二就好了。
时间复杂度 \(O(3^n)\),瓶颈在枚举子集。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N = 25,V=(1<<18),mod=998244353;
int u[N*N],v[N*N];
vector<int>e[N];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
int f[V],g[V],ans[N],cnt[V];
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=1;i<=m;i++){
cin>>u[i]>>v[i];
e[u[i]].push_back(v[i]);
e[v[i]].push_back(u[i]);
}
for(int i=0;i<(1<<n);i++){
for(int j=1;j<=m;j++){
cnt[i]+=(((i>>(u[j]-1))&1)&((i>>(v[j]-1))&1));
}
}
for(int s=0;s<(1<<n);s++){
for(int u=s;u>=0;u=(u-1)&s){
int cm=cnt[s]-cnt[u]-cnt[s^u];
g[s]+=qpow(2,cm);
if(!u)break;
}
}
for(int s=0;s<(1<<n);s++){
int v=(s&(-s));
int mk=s^v,x=0;
f[s]=g[s];
for(int j=(mk-1)&mk;j>=0;j=(j-1)&mk){
if(j!=mk){
int t=j|v;
x=(x+f[t]%mod*g[s^t]%mod)%mod;
}
if(!j)break;
}
f[s]=(f[s]-x+mod)%mod;
}
int ans=f[(1<<n)-1];
ans=(ans%mod*qpow(2,mod-2)%mod)%mod;
cout<<ans;
}
有向图容斥计数
DAG 生成子图计数
问一个有向图有几个生成子图是有向无环图,考虑 DAG 的性质,那就是会有入度为 \(0\) 的点。
定义 \(f(S)\) 为点集 \(S\) 形成有向无环图的方案数,我们可以考虑枚举哪些点是源点来转移。令 \(T \subseteq S\) 为源点点集,那么乍一看就有转移 \(f(S)=\sum_{\emptyset \neq T \subseteq S}2^{E(T,S/T)}f(S/T)\)。这个式子表示选出的源点集合向其余的点集 \(S/T\) 连边,\(S/T\) 也要是 DAG,而 \(T\) 向 \(S/T\) 的边随便连不连。
但这是错误的,因为即使源点集合不一样,他们所形成的图结构可能相同,所以就算重了。
考虑容斥,一个经典的想法就是奇数加偶数减,即 \(f(S)=\sum_{\emptyset \neq T \subseteq S}(-1)^{|T|+1}2^{E(T,S/T)}f(S/T)\),考虑证明这是对的。
对于一个有 \(k\) 个源点的特定 DAG 结构来说,只要枚举的 \(T\) 是 \(k\) 个点的子集就会把这个图计算一次,对于大小为 \(i\) 的 \(T\) 来说,有 \(\binom{k}{i}\) 种 \(T\),那就计入这个图 \(\binom{k}{i}(-1)^{i+1}\) 次。
总次数为 \(\sum_{i=1}^k \binom{k}{i} (-1)^{i+1}\),由二项式定理可知,\((1-1)^k=\sum_{i=0}^k\binom{k}{i}(-1)^i\),所以 \(\binom{k}{0}-\binom{k}{1}+\binom{k}{2}-...+\binom{k}{k}(-1)^k = 0\),移项得 \(\binom{k}{1}-\binom{k}{2}+...+\binom{k}{k}(-1)^{k+1} = \binom{k}{0} = 1\),发现这个式子就是我们刚才写出的总次数,所以每个点只会被计入一次,证明完毕。
这其实就是一个子集反演,具体推导可以去主旋律那个题的题解区看。
写了一个 \(O(3^nn)\) 的做法,在算 \(E(S,S/T)\) 的时候多了一个 \(n\),因为没题所以没测,有锅的话踢我。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N = 25,V=(1<<18),mod=998244353;
int arv[N],f[V],pw[N*N],cnt[N][V];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
pw[0]=1;
for(int i=1;i<=n*n;i++){
pw[i]=(pw[i-1]*2)%mod;
}
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
arv[u-1]|=(1<<(v-1));
}
for(int i=0;i<n;i++){
for(int s=0;s<(1<<n);s++){
cnt[i][s]=popcount(arv[i]&s);
}
}
f[0]=1;
for(int s=0;s<(1<<n);s++){
for(int t=s;t;t=(t-1)&s){
int e=0,tmp=t;
while(tmp){
int v=__builtin_ctz(tmp);
e+=cnt[v][s^t];
tmp&=(tmp-1);
}
int typ=((popcount(t))&1)?1:mod-1;
f[s]=(f[s]+typ*pw[e]%mod*f[s^t]%mod)%mod;
}
}
cout<<f[(1<<n)-1];
}
P11714 [清华集训 2014] 主旋律
这题是在上一个题的基础上得来的,询问你一个给定的有向图有几个生成子图是强连通的。
设 \(f(S)\) 为 \(S\) 的生成子图为强连通的方案数,\(g(S)\) 为所有生成子图个数。显然 \(g(S) = 2^{cnt_S}\)。
既然要列式子还是从不同角度考虑,还有什么方法可以表示 \(g(S)\) 呢?我们知道,任何一个有向图缩点之后都会变成 DAG,既然是 DAG,就可以和我们刚才练的 DAG 计数联系起来。
上一题所说 DAG 的源点,此刻变成了所有入度为 \(0\) 的 SCC,那我们就枚举子集 \(T\) 作为构成这些 SCC 的点,类比 DAG 计数可以得到一个容斥式子。\(g(S)=\sum_{\emptyset \neq T \subseteq S}H(T)\times2^{E(T,S/T)}\times g(S/T)\),其中 \(H(T)\) 应该包含容斥系数,我们来考虑它是什么。
设 \(T\) 一共构成了 \(k\) 个 SCC,那么系数应该是 \((-1)^{k+1}\),但是 \(H(T)\) 还应该乘上怎么划分成 \(k\) 个 SCC 的方案数。所以总结一下 \(H(S)\) 的定义为 \(S\) 作为源点 SCC 点集时,划分出奇数个 SCC 的方案数减去划分出偶数个 SCC 的方案数。
既然提到了划分 SCC,那我们就把 \(H(S)\) 和我们一直没管了的 \(f(S)\) 挂上了钩。考虑写出这两个的关系式,当所有点在同一个 SCC 里时,方案数为 \(f(S)\),否则我们就考虑固定一个点 \(v\),枚举一个包含 \(v\) 的子集 \(T\) 为一个 SCC,这是我们为了不算重的老套路了。此时方案数为 \(\sum_{v \in T \subset S}f(T)\times (-H(S/T))\),因为多了一个 SCC,奇偶性翻转了,原来的 \(H(S/T)\) 的系数都要反过来。总式就是 \(H(S)=f(S)-\sum_{v \in T \subset S}f(T)\times H(S/T)\)。
那我们现在就可以做了呀,已知 \(g(S)\) 的话,就先把 \(H(S)\) 算出来,由前面那个式子移项得 \(H(S)=g(S)-\sum_{\emptyset \neq T \subset S}H(T)\times 2^{E(T,S/T)} g(S/T)\)。
知道了 \(H(S)\) 就可以算 \(f(S)\) 了,还是变一下式子,得到 \(f(S)=H(S)+\sum_{v \in T \subset S}f(T)\times H(S/T)\)。
代码是简单的,复杂度 \(O(n3^n)\),已经可以过了。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N = 25,V=(1<<18),mod=1e9+7;
int arv[N],f[V],h[V],g[V],pw[N*N],e[N][V],cnt[V];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
pw[0]=1;
for(int i=1;i<=n*n;i++){
pw[i]=(pw[i-1]*2)%mod;
}
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
arv[u-1]|=(1<<(v-1));
for(int s=0;s<(1<<n);s++){
cnt[s]+=((s>>(u-1)&1)&(s>>(v-1)&1));
}
}
for(int i=0;i<n;i++){
for(int s=0;s<(1<<n);s++){
e[i][s]=popcount(arv[i]&s);
}
}
for(int s=0;s<(1<<n);s++){
g[s]=pw[cnt[s]];
}
for(int s=0;s<(1<<n);s++){
h[s]=g[s];
int x=0;
for(int t=(s-1)&s;t;t=(t-1)&s){
int E=0,tmp=t;
while(tmp){
int v=__builtin_ctz(tmp);
E+=e[v][s^t];
tmp&=(tmp-1);
}
x=(x+h[t]%mod*pw[E]%mod*g[s^t]%mod)%mod;
}
h[s]=(h[s]-x+mod)%mod;
int v=(s&(-s)),mk=s^v;x=0;
for(int j=(mk-1)&mk;j>=0;j=(j-1)&mk){
int t=j|v;
x=(x+f[t]%mod*h[s^t]%mod)%mod;
if(!j)break;
}
f[s]=(h[s]+x)%mod;
}
cout<<f[(1<<n)-1];
}
其实优化到 \(O(3^n)\) 也不算难事,这个 \(n\) 是计算 \(E(T,S/T)\) 时带上的,考虑令 \(e_T\) 表示在全集为 \(S\) 时,\(T\) 连向 \(S/T\) 的边数。这个可以用增量更新。每一次 \(T\) 将一个点 \(x\) 丢给 \(S/T\) 的时候,\(e_T\) 都减去 \(x\) 到原来 \(S/T\) 的边数,加上原来 \(T\) 到 \(x\) 的边数即可。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N = 25,V=(1<<18),mod=1e9+7;
int arv[N],f[V],h[V],g[V],pw[N*N],e[V],cnt[V],in[N],out[N],id[V];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
int lowbit(int x){
return x&(-x);
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n>>m;
pw[0]=1;
for(int i=1;i<=n*n;i++){
pw[i]=(pw[i-1]*2)%mod;
}
for(int i=0;i<n;i++)id[1<<i]=i+1;
for(int i=1;i<=m;i++){
int u,v;
cin>>u>>v;
in[v]|=(1<<(u-1));
out[u]|=(1<<(v-1));
for(int s=0;s<(1<<n);s++){
cnt[s]+=((s>>(u-1)&1)&(s>>(v-1)&1));
}
}
for(int s=0;s<(1<<n);s++){
g[s]=pw[cnt[s]];
}
for(int s=0;s<(1<<n);s++){
h[s]=g[s];
int x=0;
e[s]=0;
for(int t=(s-1)&s;t;t=(t-1)&s){
int x=lowbit(s-t);
int v=id[x];
e[t]=e[t+x]-popcount(out[v]&(s-t-x))+popcount(in[v]&t);
}
for(int t=(s-1)&s;t;t=(t-1)&s){
int E=e[t];
x=(x+h[t]%mod*pw[E]%mod*g[s^t]%mod)%mod;
}
h[s]=(h[s]-x+mod)%mod;
int v=(s&(-s)),mk=s^v;x=0;
for(int j=(mk-1)&mk;j>=0;j=(j-1)&mk){
int t=j|v;
x=(x+f[t]%mod*h[s^t]%mod)%mod;
if(!j)break;
}
f[s]=(h[s]+x)%mod;
}
cout<<f[(1<<n)-1];
}
竞赛图
任意两点之间有且只有一条边的有向图称为竞赛图,询问有几个 \(n\) 个点的竞赛图是强连通的。
这里我们倒不用状压了,设 \(g(i)\) 表示 \(i\) 个点的竞赛图的个数,\(f(i)\) 表示 \(i\) 个点的强连通竞赛图的个数。\(g(i) = 2^{\frac{i\times (i-1)}{2}}\),因为共 \(\frac{i\times (i-1)}{2}\) 条边,每条边有正向反向两种选择。
首先 \(g(i)\) 加上图直接强连通的方案数 \(f(i)\),如果不是强连通的话,还是考虑把图拆分成两部分,强连通的部分和非强连通的部分,则 \(g(i)=f(i)+\sum_{j=1}^{i-1}f(j)\times g(i-j) \times \binom{i}{j}\),则 \(f(i) = g(i)-\sum_{j=1}^{i-1} f(j)\times g(i-j) \times \binom{i}{j}\)。
可以做到 \(O(n^2)\),随手写的代码,有锅踢我。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define _int __int128
#define ull unsigned long long
#define pii pair<int,int>
#define fst first
#define scd second
#define pq priority_queue
#define mkp make_pair
#define popcount(x) __builtin_popcount(x)
#define endl '\n'
int n,m;
const int N = 5005,mod=998244353;
int f[N],g[N],fac[N],inv[N];
int qpow(int a,int b){
int res=1;
while(b){
if(b&1)res=(res*a)%mod;
a=(a*a)%mod;
b>>=1;
}
return res;
}
int C(int n,int m){
if(n<m)return 0;
return fac[n]%mod*inv[m]%mod*inv[n-m]%mod;
}
signed main(){
ios::sync_with_stdio(0);
cin.tie(0),cout.tie(0);
cin>>n;
fac[0]=inv[0]=1;
for(int i=1;i<=n;i++){
fac[i]=(fac[i-1]*i)%mod;
}
inv[n]=qpow(fac[n],mod-2);
for(int i=n-1;i>=0;i--){
inv[i]=(inv[i+1]*(i+1))%mod;
}
for(int i=1;i<=n;i++){
int e=i*(i-1)/2;
g[i]=qpow(2,e);
}
for(int i=1;i<=n;i++){
f[i]=g[i];
int x=0;
for(int j=1;j<i;j++){
x=(x+f[j]%mod*g[i-j]%mod*C(i,j)%mod)%mod;
}
f[i]=(f[i]-x+mod)%mod;
}
cout<<f[n];
return 0;
}

浙公网安备 33010602011771号