正题
题目链接: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;
}