子集反演学习笔记
子集反演学习笔记
跟着亓爷爷学的子集反演,例题也全部都是亓爷爷的例题,所以先给出亓爷爷的博客: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)\) 好求那么就可以应用子集反演。
亓爷爷没写证明,我来补一下证明(也是贺的别人的证明):
其中:
所以:
下面是例题(全是亓爷爷的例题)
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)\) 全部加起来,也就是:
发现这有两个 \(\sum\) 不好求,我们考虑交换求和符号,然后用上面证明中用到的那个关于 \(h(S)\) (与现在的 \(h\) 不是一个意思)的式子:
这样我们就可以一个图的删边 \(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;
}