Pbri

子集反演学习笔记

子集反演学习笔记

跟着亓爷爷学的子集反演,例题也全部都是亓爷爷的例题,所以先给出亓爷爷的博客: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)\) 表示 \(S\subseteq A\) 的答案,那么我们钦定选择了 \(S\) 这个集合中的某个子集 \(T\) ,就应该有 \(g(S)=\sum_{T\subseteq S}f(T)\) ,这时子集反演给出 \(f(S)=\sum_{T\subseteq S}(-1)^{|S|-|T|}g(T)\)

类似的,我们有:设 \(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} \]

其中:

\[\begin{aligned} &h(S)\\ =&\sum_{T\subseteq S}(-1)^{|S|-|T|}\\ =&\sum_{i=0}^{|S|}\dbinom{|S|}{i}(-1)^{|S|-i}\\ =&(1-1)^{|S|}\\ =&[S=\varnothing]\\ \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;
}

posted @ 2021-10-20 17:49  Pbri  阅读(2219)  评论(0编辑  收藏  举报