子集反演學習筆記
子集反演學習筆記
跟著亓爺爺學的子集反演,例題也全部都是亓爺爺的例題,所以先給出亓爺爺的部落格:https://shanlunjiajian.github.io/2021/10/18/subset-inversion/
證明的來源是:https://www.cnblogs.com/wxywxywxy/p/15205488.html
本文進行了補充和複述/cy
下面的話我基本是複製亓爺爺的部落格,感覺亓爺爺已經概括的足夠精妙了。
讓我們先了解一下子集反演解決怎樣的問題:在恰好是某個集合和至少/至多是這個集合切換。
如果我們有一個特定的符合要求的集合 \(A\) ,設 \(f(S)\) 表示 \(A=S\) 的答案,設 \(g(S)\)
類似的,我們有:設 \(f(S)\) 表示 \(S=A\) 的答案, \(g(S)\) 表示 \(A\subseteq S\) (注意這裡反過來了)的答案,那麼我們欽定選擇了包含這個集合的某個集合,就應該有 \(g(S)=\sum_{S\subseteq T}f(T)\) ,這時子集反演給出 \(f(S)=\sum_{S\subseteq T}(-1)^{|T|-|S|}g(T)\)
如果直接求 \(f(S)\) 不好求但 \(g(S)\) 好求那麼就可以應用子集反演。
亓爺爺沒寫證明,我來補一下證明(也是賀的別人的證明):
\[\begin{aligned} &\sum_{T\subseteq S}(-1)^{|S|-|T|}g(T)\\ =&\sum_{T\subseteq S}(-1)^{|S|-|T|}\sum_{Q\subseteq T\subseteq S}f(Q)\\ =&\sum_{Q\subseteq S}f(Q)\sum_{Q\subseteq T\subseteq S}(-1)^{|S|-|T|}\\ =&\sum_{Q\subseteq S}f(Q)\sum_{T\subseteq {S-Q}}(-1)^{|S-Q|-{T}}\\ =&\sum_{Q\subseteq S}f(Q)h(S-Q) \end{aligned} \]其中:
所以:
\[\sum_{Q\subseteq S}f(Q)h(S-Q)=\sum_{Q\subseteq S}f(Q)[S-Q=\varnothing]=f(S) \]下面是例題(全是亓爺爺的例題)
P3349 [ZJOI2016]小星星
題意
說給一張 \(n\) 個點的無重邊無自環的無向圖,給一棵 \(n\) 個點的樹,然後你現在要給這棵樹重標號,問有多少種重標號的方案使得這棵樹是原圖的一棵生成樹。 \(n\le 17\)
題解
考慮說我們要用集合 \(S\) 這個集合給每個結點重標號,並且是每個元素至少用一次,設答案為 \(f(S)\)。那麼我們可以考慮類似的,我們只用集合 \(S\) 中的編號來重編號,這構成了至多用這個集合,設答案為 \(g(S)\) 。那麼我們最終的答案應該是 \(f(\{1,2,3...n\})\) ,為什麼定義裡面沒說每個編號最多用一次卻仍然正確呢,因為 \(n\) 個編號每個編號至少用一遍,有 \(n\) 個結點要用,根據鴿籠原理每個編號恰好只用了一遍,所以他構成了恰好用這個集合。那麼我們去欽定用了這個集合中的哪個子集,就應該有 \(g(S)=\sum_{T\subseteq S}f(T)\) ,根據子集反演我們就有 \(f(S)=\sum_{T\subseteq S}(-1)^{|S|-|T|}g(T)\) 。接下來的問題變成了如何去求 \(g(S)\) 。
我們可以定義 \(dp(u,x,S)\) 表示用 \(S\) 集合去給 \(u\) 的子樹重編號,\(u\) 的編號是 \(x\) 。然後就是樸素的樹形 \(dp\) ,如果在原圖中 \((x,y)\in E\) ,那麼就可以由 \(dp(v,y,S)\) 向 \((u,x,S)\) 轉移。可以不記錄最後一層,因為只有相同的 \(S\) 間才會相互轉移。
\(code\) :
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdlib>
#define FUP(i,x,y) for(int i=(x);i<=(y);i++)
#define FDW(i,x,y) for(int i=(x);i>=(y);i--)
#define FED(i,x) for(int i=head[x];i;i=ed[i].nxt)
#define pr pair<int,int>
#define mkp(a,b) make_pair(a,b)
#define fi first
#define se second
#define pb(x) push_back(x)
#define MAXN 20
#define INF 0x3f3f3f3f
#define LLINF 0x3f3f3f3f3f3f3f3f
#define eps 1e-9
#define MOD 1000000007
#define ll long long
#define db double
using namespace std;
int read()
{
int w=0,flg=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-'){flg=-1;}ch=getchar();}
while(ch<='9'&&ch>='0'){w=w*10+ch-'0',ch=getchar();}
return w*flg;
}
int head[MAXN],ednum;
struct edge{
int nxt,to;
}ed[MAXN*MAXN];
void add_Edge(int u,int v)
{
ednum++;
ed[ednum].nxt=head[u],ed[ednum].to=v;
head[u]=ednum;
}
int n,m,p[MAXN],tot,cnt[1<<17];
bool E[MAXN][MAXN];
ll ans,dp[MAXN][MAXN];
void dfs(int u,int fa,int S)
{
FUP(i,1,tot) dp[u][p[i]]=1;
FED(i,u)
{
int v=ed[i].to;
if(v==fa) continue;
dfs(v,u,S);
FUP(j,1,tot)
{
ll tmp=0;
FUP(k,1,tot) if(E[p[j]][p[k]]) tmp+=dp[v][p[k]];
dp[u][p[j]]*=tmp;
}
}
}
int main(){
n=read(),m=read();
FUP(i,1,m)
{
int u=read(),v=read();
E[u][v]=E[v][u]=1;
}
FUP(i,1,n-1)
{
int u=read(),v=read();
add_Edge(u,v),add_Edge(v,u);
}
FUP(i,0,(1<<n)-1)
{
cnt[i]=cnt[i>>1]+(i&1),tot=0;
FUP(j,0,n-1) if(i&(1<<j)) p[++tot]=j+1;
dfs(1,0,i);
ll sum=0;
FUP(j,1,tot) sum+=dp[1][p[j]];
ans+=(n-cnt[i])&1?-sum:sum;
}
printf("%lld\n",ans);
return 0;
}
P4336 [SHOI2016]黑暗前的幻想鄉
題意
說有 \(n\) 個點的完全圖,有 \(n-1\) 種顏色,每種顏色可以對這個圖中的某些邊染色,每種顏色染且僅染一條邊,問有多少種染色方案可以使得被染的邊是原圖中的一棵生成樹。 \(n\le 17\)
題解
類似小星星,我們可以定義 \(f(S)\) 表示這個集合每種顏色至少用一遍, \(g(S)\) 表示最多用這個集合裡的顏色。這樣 \(f(\{1,2,3..n\})=\sum_{T}(-1)^{n-|T|}g(T)\) 。接下來每種顏色就等價了,直接矩陣樹算生成樹數量即可。
\(code\) :
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <bitset>
#define FUP(i,x,y) for(int i=(x);i<=(y);i++)
#define FDW(i,x,y) for(int i=(x);i>=(y);i--)
#define FED(i,x) for(int i=head[x];i;i=ed[i].nxt)
#define pr pair<int,int>
#define mkp(a,b) make_pair(a,b)
#define fi first
#define se second
#define pb(x) push_back(x)
#define MAXN 20
#define INF 0x3f3f3f3f
#define LLINF 0x3f3f3f3f3f3f3f3f
#define eps 1e-9
#define MOD 1000000007
#define ll long long
#define db double
using namespace std;
int read()
{
int w=0,flg=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-'){flg=-1;}ch=getchar();}
while(ch<='9'&&ch>='0'){w=w*10+ch-'0',ch=getchar();}
return w*flg;
}
const bool is_print=0;
ll poww(ll a,ll b)
{
ll ans=1,base=a;
while(b)
{
if(b&1) ans=ans*base%MOD;
base=base*base%MOD,b>>=1;
}
return ans;
}
int n,tot[MAXN],ed[MAXN][MAXN*MAXN][2],cnt[1<<16];
ll ans,D[MAXN][MAXN],E[MAXN][MAXN],K[MAXN][MAXN];
ll solve()
{
int fh=1;
FUP(i,1,n-1)
{
int d=i;
FUP(j,i,n-1) if(K[j][i]){d=i;break;}
if(!K[d][i]) return 0;
if(i!=d) swap(K[d],K[i]),fh=MOD-fh;
ll inv=poww(K[i][i],MOD-2);
FUP(j,i+1,n-1)
{
ll mul=K[j][i]*inv%MOD;
FUP(k,i,n-1) K[j][k]=(K[j][k]-K[i][k]*mul%MOD+MOD)%MOD;
}
}
ll re=fh;
FUP(i,1,n-1) re=re*K[i][i]%MOD;
return re;
}
int main(){
n=read();
FUP(i,1,n-1)
{
tot[i]=read();
FUP(j,1,tot[i]) ed[i][j][0]=read(),ed[i][j][1]=read();
}
FUP(i,0,(1<<(n-1))-1)
{
if(is_print) cout<<(bitset<3>)i<<endl;
cnt[i]=cnt[i>>1]+(i&1);
FUP(j,1,n) FUP(k,1,n) D[j][k]=E[j][k]=K[j][k]=0;
FUP(j,0,n-2)
{
if(i&(1<<j))
{
FUP(k,1,tot[j+1])
{
int u=ed[j+1][k][0],v=ed[j+1][k][1];
D[u][u]++,D[v][v]++,E[u][v]++,E[v][u]++;
}
}
}
FUP(j,1,n-1) FUP(k,1,n-1) K[j][k]=(D[j][k]+MOD-E[j][k])%MOD;
ll re=solve();
if((n-1-cnt[i])&1) ans=(ans-re+MOD)%MOD;
else ans=(ans+re)%MOD;
if(is_print) printf("re=%lld cnt=%d ans=%lld\n",re,cnt[i],ans);
}
printf("%lld\n",ans);
return 0;
}
UOJ#37. 【清華集訓2014】主旋律
題意
給一張強連通圖,問有多少種刪邊的方案滿足刪完之後仍然是個強連通圖。 \(n\le 15\)
題解
考慮正難則反,用 \(2^m\) 減去不是強連通圖的方案數,即按這種刪邊方案刪完的圖縮完點只有一個點。那麼我們有一種暴力的做法,列舉所有的縮點方案,然後對縮完點後的圖求:有多少種刪邊方案使得剩下的圖是個 \(DAG\) 。
方法是說,因為是個 \(DAG\) ,所以一定有一些入度為 \(0\) 的點,那麼我們可以定義 \(f(T,S)\) 表示 \(S\) 集合中 \(T\) 恰好是所有入度為 \(0\) 的點的 \(DAG\) 子圖數量,然後 \(g(T,S)\) 表示 \(S\) 集合中 \(T\) 集合中的點一定入度為 \(0\) ,\(S-T\) 中的點無所謂。那麼我們應該有 \(g(T,S)=\sum_{T\subseteq R\subseteq S}f(R,S)\) ,子集反演得到 \(f(T,S)=\sum_{T\subseteq R\subseteq S}(-1)^{|R|-|T|}g(R,S)\) 。
然後考慮如何快速求出 \(g(T,S)\) ,令 \(h(S)\) 表示 \(S\) 這個集合的刪邊 \(DAG\) 子圖數量也就是目前這個子問題的答案,令 \(c(S1,S2)\) 表示 \(\sum_{u\in S1,v\in S2}[(u,v)\in E]\) ,也就是由 \(S1\) 指向 \(S2\) 的邊的數量。就應該有 \(g(T,S)=2^{c(T,S-T)}h(S-T)\) 。我們需要求的是 \(h(S)\) ,我們思考他如何轉移:列舉所有的入度為 \(0\) 的點的集合 \(T\) ,然後把他們的 \(f(T,S)\) 全部加起來,也就是:
\[h(S)=\sum_{T\subseteq S,T\ne \varnothing}f(T,S)=\sum_{T\subseteq S,T\ne \varnothing}\sum_{T\subseteq R\subseteq S}(-1)^{|R|-|T|}g(R,S) \]發現這有兩個 \(\sum\) 不好求,我們考慮交換求和符號,然後用上面證明中用到的那個關於 \(h(S)\) (與現在的 \(h\) 不是一個意思)的式子:
\[\begin{aligned} &\sum_{T\subseteq S,T\ne \varnothing}\sum_{T\subseteq R\subseteq S}(-1)^{|R|-|T|}g(R,S)\\ =&\sum_{R\subseteq S,R\ne \varnothing}(-1)^{|R|}g(R,S)\sum_{T\subseteq R\subseteq S,T\ne \varnothing} (-1)^{|T|}\\ =&\sum_{R\subseteq S,R\ne \varnothing}(-1)^{|R|}g(R,S)([R=\varnothing]-1)\\ =&\sum_{R\subseteq S,R\ne \varnothing}(-1)^{|R|+1}g(R,S) \end{aligned} \]這樣我們就可以一個圖的刪邊 \(DAG\) 數量這個子問題了。
回到我們的原問題來,我們目前樸素的思路是列舉所有的縮點方案,然後暴力統計刪邊 \(DAG\) 子圖的數量,但這樣複雜度還是要上天。我們觀察我們的答案,他應該是形如 \(\sum_{所有縮點方案}\sum_{R\subseteq S} \sum_{R\subseteq S,R\ne \varnothing}(-1)^{|R|+1}g(R,S)\) ,我們交換求和順序,我們先去列舉集合 \(R\) ,再去列舉集合 \(R\) 縮點方案,然後再去列舉 \(S-R\) 的縮點縮成 \(DAG\) 的方案,這樣我們對第二部分和第三部分優化,發現第二部分我們並不關心他縮成了什麼,帶上容斥係數之後我們只關心縮成奇數個 \(SCC\) 的方案數減去偶數個 \(SCC\) 的方案數。對於第三部分,列舉縮成的 \(DAG\) 然後又把點拆開,這相當於枚舉了所有的子圖,所以方案數就是 \(2^{w(S-R,S-R)}\) 。然後我們定義 \(G(S)\) 表示 \(S\) 縮成奇數個的方案數減去縮成偶數個的方案數,然後令 \(dp(S)\) 表示刪邊 \(SCC\) 子圖數量,那麼我們的轉移應該就是:
\(dp(S)=2^{w(S,S)}-\sum_{T\subseteq S,T\ne \varnothing}G(T)2^{w(T,S-T)+w(S-T,S-T)}\)
\(G(S)=dp(S)-\sum_{T\subseteq S,p\in T}G(S-T)\times dp(T)\)
下面這個轉移是說我們列舉子集,去掉他,然後剩下的子集的奇數減偶數因為還要再加上這個子集縮成的 \(SCC\) 所以奇偶性改變,然後因為一種方案會被他的多個 \(SCC\) 都列舉到,所以我們列舉去掉的是包含某個元素的子集。看起來會相互轉移,實際上只有一個 \(SCC\) 的時候不應該被轉移到 \(dp\) 裡,所以我們可以先轉移 \(dp\) 再轉移 \(G\) ,最後我們還要加上所有的 \(SCC\) 間不連通也就是全是入度為 \(0\) 的 \(SCC\) ,所以還要減去 \(G(S)\) 。
\(code\) :
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
#include <cstring>
#include <cstdlib>
#include <bitset>
#define FUP(i,x,y) for(int i=(x);i<=(y);i++)
#define FDW(i,x,y) for(int i=(x);i>=(y);i--)
#define FED(i,x) for(int i=head[x];i;i=ed[i].nxt)
#define pr pair<int,int>
#define mkp(a,b) make_pair(a,b)
#define fi first
#define se second
#define pb(x) push_back(x)
#define MAXN 100010
#define INF 0x3f3f3f3f
#define LLINF 0x3f3f3f3f3f3f3f3f
#define eps 1e-9
#define MOD 1000000007
#define ll long long
#define db double
using namespace std;
int read()
{
int w=0,flg=1;
char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-'){flg=-1;}ch=getchar();}
while(ch<='9'&&ch>='0'){w=w*10+ch-'0',ch=getchar();}
return w*flg;
}
const bool is_print=0;
int n,m,in[20],out[20],pw[500],lg[1<<15],cnt[1<<15],etot[1<<15],w[1<<15];
ll dp[1<<15],del[1<<15];
int lowbit(int x){return x&(-x);}
int main(){
n=read(),m=read(),pw[0]=1;
FUP(i,1,m)
{
int u=read(),v=read();
in[v]^=1<<(u-1),out[u]^=1<<(v-1);
}
FUP(i,1,m) pw[i]=(pw[i-1]<<1)%MOD;
FUP(i,0,n-1) lg[1<<i]=i+1;
FUP(i,1,(1<<n)-1) cnt[i]=cnt[i>>1]+(i&1);
FUP(i,1,(1<<n)-1)
{
int lbt=lowbit(i),p=lg[lbt];
etot[i]=etot[i^lbt]+cnt[out[p]&i]+cnt[in[p]&i];
if(is_print) cout<<(bitset<3>)i<<" "<<"etot="<<etot[i]<<endl;
}
FUP(S,1,(1<<n)-1)
{
dp[S]=pw[etot[S]];
if(is_print) cout<<"S="<<(bitset<3>)S<<endl;
int id=lowbit(S);
for(int T=S;T;T=(T-1)&S)
{
if(is_print) cout<<"T="<<(bitset<3>)T<<endl;
int p=lg[lowbit(S^T)];
if(p) w[T]=w[T^(1<<(p-1))]-cnt[out[p]&(S^T)]+cnt[in[p]&T];
else w[T]=0;
if(is_print) printf("w=%d\n",w[T]);
dp[S]=(dp[S]+MOD-del[T]*pw[w[T]]%MOD*pw[etot[S^T]]%MOD)%MOD;
if(!(T&id)) continue;
del[S]=(del[S]-dp[T]*del[S^T]%MOD+MOD)%MOD;
}
dp[S]=(dp[S]-del[S]+MOD)%MOD;
del[S]=(del[S]+dp[S])%MOD;
if(is_print) printf("dp=%lld del=%lld\n",dp[S],del[S]);
}
printf("%lld\n",dp[(1<<n)-1]);
return 0;
}