[做題筆記] 淺談狀壓dp在圖計數問題上的應用
無向圖計數
題目描述
有一個 \(n\) 個點 \(m\) 條邊的無向圖,對於每個 \(k\) 求出有多少種保留邊的方案使得 \(1\) 能到 \(k\)
\(n\leq 17,m\leq {n\choose 2}\)
解法
設 \(dp[s]\) 表示 \(1\) 能到集合 \(s\),只考慮集合 \(s\) 中的邊的方案數,轉移考慮總方案減去不合法的方案,不合法的方案可以列舉一個 \(s\) 的真子集 \(t\),那麼 \(s\oplus t\) 和 \(t\) 之間不能有邊,\(s\oplus t\) 邊任意,設 \(cnt[s]\) 表示 \(2\) 的 \(s\) 內部邊數次方:
可以用 \(\tt FWT\) 優化,時間複雜度 \(O(3^n)/O(2^nn)\)
總結
正難則反是考慮連通性問題的一類重要方法。
#pragma GCC optimize(2) #include <cstdio> const int M = 18; const int MOD = 998244353; #define int long long int read() { int x=0,flag=1;char c; while((c=getchar())<'0' || c>'9') if(c=='-') flag=-1; while(c>='0' && c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar(); return x*flag; } int n,m,g[M][M],ans[M],dp[1<<M],cnt[1<<M]; signed main() { n=read();m=read(); for(int i=1;i<=m;i++) { int u=read()-1,v=read()-1; g[u][v]=g[v][u]=1; } for(int s=0;s<(1<<n);s++) { cnt[s]=1; for(int i=0;i<n;i++) for(int j=i+1;j<n;j++) if((s&(1<<i)) && (s&(1<<j)) && g[i][j]) cnt[s]=2*cnt[s]%MOD; } dp[0]=1; for(int s=1;s<(1<<n);s++) { if(s&1) dp[s]=cnt[s]; for(int t=(s-1)&s;t;t=(t-1)&s) dp[s]=(dp[s]-dp[t]*cnt[s-t])%MOD; } for(int s=0;s<(1<<n);s++) for(int i=1;i<n;i++) if(s&(1<<i)) ans[i]=(ans[i]+dp[s]*cnt[(1<<n)-1-s])%MOD; for(int i=1;i<n;i++) printf("%lld\n",(ans[i]+MOD)%MOD); }
強連通計數
題目描述
解法
這個題很好,有向圖計數問題可以轉 \(\tt DAG\) 計數,因為那東西已經有一套成熟的計算方法了。
設 \(f[s]\) 表示只考慮集合 \(s\) 的點和它們的邊的方案數,還是總數減去不合法的方案數,計算不合法的方案數直接轉 \(\tt DAG\) 計數,也就是考慮不合法的方案意味著縮點之後構成了 \(\tt DAG\),並且點數大於 \(1\)
那麼我們對入度為 \(0\) 的點容斥,設 \(g[s]\) 是欽定集合 \(s\) 中的點入度為 \(0\) 的方案數,轉移:
\[g[s]=-\sum_{t\in s}g[t]\times f[s-t] \]也就是每次新新增一個入度為 \(0\)
設 \(sn[s]\) 表示集合 \(i\) 連向集合 \(s\) 的邊,這個每次 \(dp\) 的時候需要線上處理出來,設 \(s[i]\) 表示集合 \(i\) 內部的邊數,那麼欽定一個子集入度為 \(0\) 來轉移,其他的邊可以隨便選:
\[f[i]=2^{s[i]}-\sum_{j\in i} g[j]\times 2^{s[i]-sn[j]} \]其中 \(j\) 列舉的是非空子集,轉移完 \(f\) 之後讓 \(g[i]\leftarrow g[i]+f[i]\) 當作一個小的初始化,時間複雜度 \(O(3^n)\)
#include <cstdio>
const int M = 15;
const int N = 1<<15;
const int MOD = 1e9+7;
#define int long long
int read()
{
int x=0,flag=1;char c;
while((c=getchar())<'0' || c>'9') if(c=='-') flag=-1;
while(c>='0' && c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*flag;
}
int n,m,in[N],out[N],pw[N],cnt[N],s[N],sn[M],f[N],g[N];
void calc(int nw,int i)
{
int to=(nw-1)&i;
if(to) calc(to,i);
sn[nw]=sn[nw-(nw&(-nw))]+cnt[in[nw&(-nw)]&i];
//sn[nw]=sn[to]+cnt[in[nw-to]&i]
//what I show above is wrong,think about it
}
signed main()
{
n=read();m=read();pw[0]=1;
for(int i=1;i<=m;i++)
{
int u=read()-1,v=read()-1;
in[1<<v]|=(1<<u);
out[1<<u]|=(1<<v);
pw[i]=pw[i-1]*2%MOD;
}
for(int i=1;i<(1<<n);i++)
cnt[i]=cnt[i>>1]+(i&1);
for(int i=1;i<(1<<n);i++)
{
int w=i&(-i);
if(i!=w) s[i]=s[i-w]+cnt[in[w]&i]+cnt[out[w]&i];
}
for(int i=1;i<(1<<n);i++)
{
calc(i,i);int t=i-(i&(-i));
for(int j=(i-1)&i;j;j=(j-1)&t)
g[i]=(g[i]-g[j]*f[i-j])%MOD;
f[i]=pw[s[i]];
for(int j=i;j;j=(j-1)&i)
f[i]=(f[i]-g[j]*pw[s[i]-sn[j]])%MOD;
g[i]=(g[i]+f[i])%MOD;
}
printf("%lld\n",(f[(1<<n)-1]+MOD)%MOD);
}
二分圖計數
題目描述
給一張 \(n\) 個點 \(m\) 條邊的無向圖,問有多少種保留邊的方案使得最後的圖是聯通二分圖。
\(n\le1 7,m\leq{n\choose 2}\)
解法
這題有兩個限制,一個是二分圖,一個是聯通。
考慮分別解決限制,首先考慮集合 \(s\) 是二分圖的方案數,顯然的思路是列舉一個子集 \(t\),然後考慮兩個集合任意連邊,但是這樣顯然會算重,非連通二分圖會被統計多次。
很難去重,但是考慮我們算的方案是具有組合意義的,我們求出的是二分圖的染色方案,一個驚為天人的思路是 \(dp\) 二分圖的染色方案,最後求出的是連通二分圖的染色方案,所以除以 \(2\) 就得到答案了。
設 \(g[s]\) 是集合 \(s\) 的二分圖染色方案,\(f[s]\) 是集合 \(s\) 的連通二分圖染色方案,然後老正難則反了,我們列舉子集 \(t\),強制它為連通塊,為了防止算錯我們強制 \(t\) 不包含 \(\tt lowbit\) 那一位:
\[f[s]=\sum_t f[s-t]\times g[t] \]預處理集合 \(se[s]\) 表示集合 \(s\) 內部的邊數,時間複雜度 \(O(2^nm+3^n)\)
#include <cstdio>
const int N = 1<<17;
const int MOD = 998244353;
const int inv2 = (MOD+1)/2;
#define int long long
int read()
{
int x=0,flag=1;char c;
while((c=getchar())<'0' || c>'9') if(c=='-') flag=-1;
while(c>='0' && c<='9') x=(x<<3)+(x<<1)+(c^48),c=getchar();
return x*flag;
}
int n,m,cnt[N],e[N],se[N],g[N],f[N],pw[405];
signed main()
{
n=read();m=read();pw[0]=1;
for(int i=1;i<=m;i++)
{
int u=read()-1,v=read()-1;
e[i]=(1<<u)|(1<<v);
pw[i]=pw[i-1]*2%MOD;
}
for(int i=1;i<(1<<n);i++)
cnt[i]=cnt[i>>1]+(i&1);
for(int i=0;i<(1<<n);i++)
for(int j=1;j<=m;j++)
se[i]+=((i&e[j])==e[j]);
for(int i=1;i<(1<<n);i++)
{
g[i]++;
for(int j=i;j;j=(j-1)&i)
g[i]=(g[i]+pw[se[i]-se[j]-se[i^j]])%MOD;
f[i]=g[i];int t=i-(i&(-i));
for(int j=t;j;j=(j-1)&t)
f[i]=(f[i]-g[j]*f[i-j])%MOD;
}
printf("%lld\n",(f[(1<<n)-1]*inv2%MOD+MOD)%MOD);
}