DAG 容斥学习笔记
省选将至,如何翻盘?
DAG 容斥
注意到一个 DAG 去掉 \(0\) 度点后还是一个 DAG。于是我们可以对着这个 DP。
设 \(f_S\) 表示 \(S\) 这个点集作为 DAG 的方案数。设 \(\text{edge}(S,T)\) 表示 \(S\) 到\(T\) 的连边方案数,枚举 \(0\) 度点集合转移,则有:
然后我们发现这个东西很假,因为 \(S-T\) 中可能还有 \(0\) 度点。
在全集为 \(S\) 时,我们设 \(g_T\) 表示恰好 \(T\) 为 \(0\) 度点的方案数,\(h_T\) 表示钦定 \(T\) 为 \(0\) 度点的方案数,于是有:
做子集反演,可得:
用 \(g\) 重写 \(f\) 的转移:
于是可以在 \(\mathcal O(3^n)\) 的复杂度内解决。
例题
P6846 [CEOI 2019] Amusement Park
首先,注意到每个合法方案将所有边反转后和另一种方案一一对应,因此题目要求的等价于无向图定向为 DAG 的方案数 \(\times\frac{m}{2}\)。且由于边已经确定,因此 \(edge(S,T)=1\)。直接套板子,复杂度 \(\mathcal O(3^n)\)。
可以用子集卷积优化到 \(\mathcal O(2^nn^2)\),但是没快多少就是了。
有一个没见过的东西是对于每条无向边 \(x,y\),令 \(f_{2^x+2^y}=1\),则 \(f\) 的高维前缀和 \(F\) 满足 \(F_S=[S不是独立集]\)。
#include <bits/stdc++.h>
using namespace std;
constexpr int N=18,mod=998244353,inv2=499122177;
int n,m,l2[1<<N],a[N][N],e[1<<N][N],dp[1<<N],b[1<<N];
bool check(int x,int y){
for(int i=0;i<n;i++)if(y>>i&1&&e[x][i])return 0;
return 1;
}
signed main(){
clock_t _st=clock();
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
for(int i=2;i<1<<n;i++)l2[i]=l2[i>>1]+1;
for(int i=1,x,y;i<=m;i++)cin>>x>>y,a[x-1][y-1]=a[y-1][x-1]=1;
for(int i=1;i<1<<n;i++)for(int j=0;j<n;j++)e[i][j]=e[i^(i&-i)][j]+a[l2[i&-i]][j];
for(int i=1;i<1<<n;i++)b[i]=check(i,i);
for(int i=dp[0]=1;i<1<<n;i++)for(int j=i;j;j=i&j-1)if(b[j])
dp[i]+=(__builtin_popcount(j)&1?1ll:mod-1ll)*dp[i^j]%mod,
dp[i]>=mod&&(dp[i]-=mod);
cout<<1ll*dp[(1<<n)-1]*m%mod*inv2%mod<<'\n';
clock_t _ed=clock();
cerr<<(_ed-_st)*1.0/CLOCKS_PER_SEC<<'\n';
return 0;
}
P11714 [清华集训 2014] 主旋律
发现连通块不好刻画,于是考虑单步容斥,用总方案数减去不合法。不合法方案缩点后一定是 \(>1\) 个点的 DAG,于是考虑 DAG 容斥。设 \(dp_{S}\) 表示 \(S\) 为连通块的方案数,\(g_{S}\) 表示 \(S\) 被分为两两无边的 \(i\) 个连通块的方案乘上容斥系数的和,\(E(S,T)\) 表示 \(S\) 向 \(T\) 连边的个数。有转移:
然后发现 \(dp_{S}\) 转移的时候用到了 \(g_{S}\),\(g_{S}\) 转移的时候也用到了 \(dp_S\)。实际上 \(dp_{S}\) 转移的时候 \(g_{S}\) 应该减掉 \(dp_{S}\),于是改一下转移顺序即可。时间复杂度 \(\mathcal O(3^nn)\)。
再发现 \(dp_S\) 转移中的
可以直接递推得到。\(\mathcal O(2^nn)\)预处理每个 \(S\) 到每个点的边数即可做到 \(\mathcal O(3^n)\)。
代码
#include <bits/stdc++.h>
using namespace std;
constexpr int N=15,mod=1e9+7;
int n,m,e[1<<N][N],s[1<<N],dp[1<<N],g[1<<N],l2[1<<N],p2[1<<N];
bool a[N][N];
long long qpow(long long x,int y=mod-2){
long long ans=1;
for(;y;y>>=1,x=x*x%mod)if(y&1)ans=ans*x%mod;
return ans;
}
signed main(){
clock_t _st=clock();
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m;
l2[0]=-1;for(int i=1;i<1<<N;i++)l2[i]=l2[i>>1]+1;
for(int i=p2[0]=1;i<1<<N;i++)p2[i]=2ll*p2[i-1]%mod;
for(int i=1,x,y;i<=m;i++)cin>>x>>y,a[x-1][y-1]=1;
for(int i=1;i<1<<n;i++)for(int j=0;j<n;j++)e[i][j]=e[i^(i&-i)][j]+a[l2[i&-i]][j];
for(int i=1;i<1<<n;i++){
int sum=0;
for(int j=0;j<n;j++)if(i>>j&1)sum+=e[i][j];
for(int j=i&i-1;j;j=i&j-1)if((j&-j)==(i&-i))g[i]-=1ll*dp[j]*g[i^j]%mod,g[i]<0&&(g[i]+=mod);
dp[i]=p2[sum];
for(int j=i;j;j=i&j-1){
int x=i^j;
if(__builtin_popcount(x)==1)s[x]=e[i][l2[x]];
else if(x)s[x]=s[x^(x&-x)]+e[i][l2[x&-x]];
dp[i]-=1ll*g[j]*p2[s[x]]%mod;
if(dp[i]<0)dp[i]+=mod;
}
g[i]+=dp[i],g[i]>=mod&&(g[i]-=mod);
}
cout<<dp[(1<<n)-1]<<'\n';
clock_t _ed=clock();
cerr<<(_ed-_st)*1.0/CLOCKS_PER_SEC<<'\n';
return 0;
}
P10221 [省选联考 2024] 重塑时光
题目相当于给定 \(m\) 条边,问最多分成 \(k+1\) 个 DAG 的合法拓扑序个数。容易发现被分出来 DAG 间也构成一个大的 DAG。考虑 DAG 容斥,设 \(dp_{S,i}\) 表示 \(S\) 分为 \(i\) 个点集的拓扑序个数,\(g_{S,i}\) 表示 \(S\) 分为 \(i\) 个互不连边的点集的拓扑序个数,\(f_{S}\) 表示 \(S\) 作为一个点集的合法拓扑序个数,\(c(S,T)\) 表示 \(S\) 到 \(T\) 是否没有边。枚举 \(0\) 度点转移:
最后统计答案即可。复杂度 \(\mathcal O(3^nn^2)\)。复杂度瓶颈在于 \(dp\) 的转移,发现是卷积形式,写个插值可做到 \(\mathcal O(3^nn)\)。
代码
#include <bits/stdc++.h>
using namespace std;
constexpr int N=15,mod=1e9+7;
int n,m,k,a[N][N],e[1<<N][N],dp[1<<N][N+1],f[1<<N],g[1<<N][N+1];
int fac[N*N],ifac[N*N];
long long qpow(long long x,int y=mod-2){
long long ans=1;
for(;y;y>>=1,x=x*x%mod)if(y&1)ans=ans*x%mod;
return ans;
}
bool check(int x,int y){
for(int i=0;i<n;i++)if(y>>i&1&&e[x][i])return 0;
return 1;
}
long long C(int x,int y){return y>x?0:1ll*fac[x]*ifac[y]%mod*ifac[x-y]%mod;}
signed main(){
clock_t _st=clock();
ios::sync_with_stdio(false);
cin.tie(0),cout.tie(0);
cin>>n>>m>>k;
for(int i=fac[0]=1;i<N*N;i++)fac[i]=1ll*fac[i-1]*i%mod;
ifac[N*N-1]=qpow(fac[N*N-1]);for(int i=N*N-1;i>=1;i--)ifac[i-1]=1ll*ifac[i]*i%mod;
for(int i=0,x,y;i<m;i++)cin>>x>>y,a[x-1][y-1]=1;
for(int i=0;i<1<<n;i++)for(int j=0;j<n;j++)if(i>>j&1)for(int k=0;k<n;k++)if(a[j][k])e[i][k]++;
for(int i=f[0]=1;i<1<<n;i++)for(int j=0;j<n;j++)if(i>>j&1&&!e[i^1<<j][j])
f[i]+=f[i^1<<j],f[i]>=mod&&(f[i]-=mod);
for(int i=g[0][0]=dp[0][0]=1;i<1<<n;i++){
for(int j=i;j;j=i&j-1)if((i&-i)==(j&-j)){
int x=i^j;
if(check(j,x)&&check(x,j))for(int k=0;k<=__builtin_popcount(x);k++)
g[i][k+1]+=1ll*g[x][k]*f[j]%mod,g[i][k+1]>=mod&&(g[i][k+1]-=mod);
}
for(int j=i;j;j=i&j-1){
int x=i^j;
if(check(x,j))for(int k=0;k<=__builtin_popcount(j);k++)for(int l=0;l<=__builtin_popcount(x);l++)
dp[i][l+k]+=(k&1?1ll:mod-1ll)*g[j][k]%mod*dp[x][l]%mod,dp[i][l+k]>=mod&&(dp[i][l+k]-=mod);
}
}
int ans=0;
for(int i=1;i<=n;i++)for(int j=0;j<i;j++)
ans+=(j&1?mod-1ll:1ll)*dp[(1<<n)-1][i]%mod*fac[i]%mod*fac[i-j+k]%mod*ifac[i-j]%mod*C(i-1,j)%mod,
ans>=mod&&(ans-=mod);
cout<<1ll*ans*ifac[n+k]%mod<<'\n';
clock_t _ed=clock();
cerr<<(_ed-_st)*1.0/CLOCKS_PER_SEC<<'\n';
return 0;
}

浙公网安备 33010602011771号