CF1193A Amusement Park 题解
一.题意:
你有一个有向图,你可以选择翻转一些边的方向使其变成一个DAG,求所有翻转方案的总翻转边的数量对\(998244353\)取模
二.思路:
一个很显然的暴力做法是枚举每条边是否翻转,并且通过拓扑排序判断是否存在环,复杂度为 \(O(n\times 2^m)\) 可以通过19pts的部分分。
正解:
手搓几组样例,我们发现翻转的方案是成对出现的:若每次选了 \(x\) 条边翻转可以得到一个DAG,那么回到原来的图上,翻转剩余的 \(m-x\) 条边也依旧是一个DAG。
那这个猜想的正确性或者说本质是什么呢?其实是下面的定理:如果一个有向图是一个DAG,那么翻转其所有边之后,其依旧是一个DAG。 这个定理是显然成立的,那么上面的猜想其实就是因为我们先翻转了 \(x\) 条边之后变成了一个DAG,然后翻转整个图也是DAG才成立的,因为你翻转了两次,所以一开始翻转的 \(x\) 条边就被翻回到原来的状态了。所以你发现这两种情况翻转的数量加起来的和是定值 \(m\) ,所以每一次的平均贡献就是 \(\frac{m}{2}\)。
所以现在这一个问题就转化成了:给你一个无向图,需要给这个无向图定向,使其最终成为一个DAG的方案数然后答案乘上 \(\frac{m}{2}\)。
看到\(n\)的范围这么小,又是求方案数,我们就考虑状态压缩dp:定义状态 \(f(S)\) 表示给点集 \(S\) 的导出无向子图定向后为DAG的方案数。考虑转移:\(S\)中必定存在出度为\(0\)的独立集,说白了就是可以找出\(S\)的子集,使得其中的点互相没有约束(连边)。思考为什么,其实是因为一个DAG必定可以进行拓扑排序,他们是充要的,如果我们回忆拓扑排序的过程,你就会发现上面的结论是显然的。所以转移就从 \(S\) 中互相没有约束的子点集转移过来,形式化的:
需要注意的是这里的\(T\)必须是一个独立集,并且\(G(|T|)\)是我们的容斥系数。那么现在的问题就是如何配容斥系数了。
Q:为什么会算重?
A:因为对于每一个独立集,我们可以选择一次全部加到集合 \(S\) 中,或者,分多次加到 \(S\) 中。
我们需要所有子集对于答案的最终贡献都是\(1\),但是在上述计算中,\(T\)的每一个子集都会出现,也就是我们需要:
可以证明的是:\(G(i)=(-1)^{i+1}\)。因为:
所以最终答案就可以彻底用程序去计算了。
现在我们来分析复杂度的问题,最终计算的时候我们通过子集枚举的方式来转移dp,此部分的复杂度为 \(O(3^n)\) ,而一开始的预处理独立集的复杂度是 \(O(m\cdot2^n)\) 所以综上,总复杂度就为 \(O(3^n+m\cdot2^n)\)。此外,有的大佬题解中还包含子集卷积优化的内容,本人水平不足,就不班门弄斧了。
三.code:
#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
#define int long long
inline int read() {
int x=0,f=1;char ch=getchar();
while(ch > '9' || ch < '0'){if(ch == '-'){f = -1;}ch = getchar();}
while(ch >= '0'&&ch <= '9'){x = x * 10 + ch - 48; ch = getchar();}
return x * f;
}
const int MOD = 998244353;
const int MAX_S = (1 << 18) + 5,MAX_E = 400 + 5;
int f[MAX_S],n,m,u[MAX_E],v[MAX_E],inv_2 = 499122177;
bool mark[MAX_S];
void add(int &x,int y)
{
x += y;
if(x >= MOD) x -= MOD;
if(x < 0) x += MOD;
}
signed main()
{
n = read();m = read();
for(int i = 1;i <= m;++i)
{
u[i] = read();
--u[i];
v[i] = read();
--v[i];
}
int up_lim = 1 << n;
for(int i = 1;i < up_lim;++i)
{
for(int j = 1;j <= m;++j)
{
if(((i >> u[j]) & 1) && ((i >> v[j]) & 1))
{
mark[i] = true;
break;
}
}
}
f[0] = 1;
for(int i = 1;i < up_lim;++i)
{
for(int j = i; j;j = (j - 1) & i)
{
if(!mark[j])
{
if(__builtin_popcount(j) & 1) add(f[i],f[i ^ j]);
else add(f[i],-f[i ^ j]);
}
}
}
int ans = f[up_lim - 1] * m % MOD * inv_2 % MOD;
std::cout << ans;
return 0;
}

浙公网安备 33010602011771号