P6846[CEOI2019]Amusement Park【状压dp,FWT】

正题

题目链接:https://www.luogu.com.cn/problem/P6846


题目大意

给出\(n\)个点\(m\)条边的一张有向图,保证两个点之间最多只有一条边。现在你可以取反一些边使得图变为一张\(DAG\),求所有方案的取反的边数和。

\(1\leq n\leq 18\)


解题思路

考虑到对于一种方案取反所有边就是另一种方案,所以每种方案的取反边数的平均值肯定是\(\frac{m}{2}\),所以我们只需要统计方案数就好了。

然后再考虑\(dp\),朴素的做法是\(O(3^n)\)的,记\(G_S\)表示集合\(S\)是否是独立集那么有

\[F_S=\sum_{T\sube S}(-1)^{|T|+1}F_{S-T}G_T \]

然后化成集合幂级数的形式就是

\[F=FG+1\Rightarrow F=\frac{1}{1-G} \]

至于集合幂级数怎么求逆,定义占位多项式

\[G'_{S,i}=\sum_{S=0}^n[count(S)=i]G_S \]

然后对于每个\(G_S\)视为一个多项式求逆。

然后求逆可以用\(O(n^2)\)的反正\(n\)很小。

对于\(a\)求逆,首先有\(a^{-1}[x_0]=\frac{1}{a[x_0]}\),然后有

\[\sum_{i=0}^na[x^i]a^{-1}[x^{n-i}]=0\Rightarrow a^{-1}[x^n]=\frac{\sum_{i=0}^{n-1}a^{-1}[x^i]a[x^{n-i}]}{a[x^0]} \]

这样就可以\(O(n^2)\)递推了。

时间复杂度:\(O(2^nn^2)\)


code

#include<cstdio>
#include<cstring>
#include<algorithm>
#define ll long long
using namespace std;
const ll N=18,P=998244353;
ll n,m,MS,c[1<<N],g[1<<N],F[N+1][1<<N],G[N+1],H[1<<N];
void FWT(ll *f,ll op){
	for(ll p=2;p<=MS;p<<=1)
		for(ll k=0,len=p>>1;k<MS;k+=p)
			for(ll i=k;i<k+len;i++)
				(f[i+len]+=f[i]*op+P)%=P;
	return;
}
signed main()
{
	scanf("%lld%lld",&n,&m);
	for(ll i=1,x,y;i<=m;i++){
		scanf("%lld%lld",&x,&y);
		x--;y--;g[(1<<x)|(1<<y)]=1;
	}
	MS=(1<<n);
	for(ll i=0;i<n;i++)
		for(ll s=0;s<MS;s++)
			if(!((s>>i)&1))g[s^(1<<i)]|=g[s];
	for(ll i=1;i<MS;i++){
		c[i]=c[i-(i&-i)]+1;
		if(!g[i])F[c[i]][i]=(c[i]&1)?1:(P-1);
	}
	F[0][0]=1;
	for(ll i=0;i<=n;i++)FWT(F[i],1);
	for(ll s=0;s<MS;s++){
		for(ll i=0;i<=n;i++)F[i][s]=P-F[i][s];
		G[0]=1;
		for(ll i=1;i<=n;i++){
			G[i]=0;
			for(ll j=1;j<=i;j++)
				(G[i]+=P-G[i-j]*F[j][s]%P)%=P;
		}
		H[s]=G[n];
//		printf("%lld ",G[n]);
	}
	FWT(H,-1);
	printf("%lld\n",(P+1)/2*H[MS-1]%P*m%P);
	return 0;
}
原文地址:https://www.cnblogs.com/QuantAsk/p/15437650.html