把博客园图标替换成自己的图标
把博客园图标替换成自己的图标end

【洛谷5933】[清华集训2012] 串珠子(子集DP)

点此看题面

  • 给定一张\(n\)个点的图,每两个点\(i,j\)之间要么不连边,要么就有\(c_{i,j}\)种连边方式。
  • 求使这张图连通的连边方案数。
  • \(n\le16\)

子集\(DP\)

很容易想到设\(g_S\)表示使子集\(S\)连通的方案数,然而这东西并不好转移,因为同种连通图根据不同的转移顺序很可能会重复计算。

所以,我们考虑设两个数组来辅助转移,用\(f_S\)表示子集\(S\)连边的总方案数,\(h_S\)表示子集\(S\)不连通的方案数。

首先\(f_S\)的转移是非常容易的,就是连通块中所有\(c_{i,j}+1\)的乘积。

\(h_S\)的转移有一个求解连通块问题方案数的基本套路,就是钦定一个点(这里方便起见可以直接选取\(lowbit(x)\))把连通块分成与它连通和与它不连通的两部分。

具体地,我们枚举\(S\)除去\(i\)之后的一个子集\(T\),那么这种情况下的方案数就应该是\(g_{S\oplus T}\times f_T\)

然后我们用\(f_S\)减去\(h_S\)便得到了\(g_S\)

代码:\(O(n^22^n)\)

#include<bits/stdc++.h>
#define Tp template<typename Ty>
#define Ts template<typename Ty,typename... Ar>
#define Reg register
#define RI Reg int
#define Con const
#define CI Con int&
#define I inline
#define W while
#define N 16
#define X 1000000007
using namespace std;
int n,c[N+5][N+5],f[1<<N],g[1<<N],h[1<<N];
int main()
{
	RI i,j,k,x,l;for(scanf("%d",&n),i=1;i<=n;++i) for(j=1;j<=n;++j) scanf("%d",&c[i][j]);
	for(l=1<<n,i=0;i^l;++i)//枚举子集
	{
		for(f[i]=1,j=1;j<=n;++j) if(i>>j-1&1) for(k=j+1;k<=n;++k) i>>k-1&1&&(f[i]=1LL*f[i]*(c[j][k]+1)%X);//枚举子集中所有边求出f
		for(x=i^(i&-i),j=x;j;j=(j-1)&x) h[i]=(1LL*g[i^j]*f[j]+h[i])%X;g[i]=(f[i]-h[i]+X)%X;//枚举不与lowbit连通的点集求出g,用f减g求出h
	}return printf("%d\n",g[l-1]),0;
}
posted @ 2021-03-27 22:40  TheLostWeak  阅读(61)  评论(0编辑  收藏  举报