LOJ #3165. 「CEOI2019」游乐园
对于原图上任意一种 \(\text{DAG}\) 的方案, 假设该方案翻转了 \(x\) 条边, 那么把所有边再翻转一遍仍然是个 \(\text{DAG}\), 并且在原图的基础上翻转了 \(m-x\) 条边. 若 \(\text{DAG}\) 数量为 \(C\), 则答案 \(\text{Ans}=C\times \dfrac{m}{2}\). 故可以将原问题规约为求 \(\text{DAG}\) 数量之和.
发现数据范围 \(1\le n\le 18\). 显然是状压 \(\text{DP}\).
设 \(f_{S}\) 表示点集 \(S\) 构成的导出子图为 \(\text{DAG}\) 的方案数. 接下来考虑如何转移 \(\text{DP}\).
一个朴素的想法是枚举每一个点, 算出点 \(u\) 加入点集合 \(S\) 时的贡献. 由于 \(\text{DP}\) 的转移是有顺序的, 可以不妨设加入的点 \(u\) 在 当前 \(\text{DAG}\) 中的入度为 \(0\). 于是点 \(u\) 对 \(f_{S\cup\{u\}}\) 的贡献为 \(f_{S}\).
但是, 上述的转移会重复. 例如, 在集合 \(S\) 中添加两个互相独立的点 \(u,v\), 则 \(f_{S\cup\{u,v\}}\) 算了两遍 \(f_{S}\). 于是可以考虑运用容斥原理去重.
运用容斥原理, 转移 \(\text{DP}\) 时枚举 \(S\) 的子集 \(T\), 并且满足 \(T\) 为独立集. 则 \(T\) 对 \(S\) 的贡献为 \((-1)^{|T|-1}f_{S\backslash T}\). 故有转移方程
\[f_{S}=\sum_{T\subseteq S}(-1)^{|T|-1}[T是独立集]f_{S\backslash T} \]暴力枚举子集转移即可. 预处理独立集的复杂度为 \(\mathrm O(2^nn^2)\), 总时间复杂度为 \(\mathrm O(2^nn^2+3^n)\).
参考代码
#include <bits/stdc++.h>
using namespace std;
static constexpr int mod = 998244353;
static constexpr int Maxn = 18;
int n, m, nn;
bool e[Maxn][Maxn];
bool d[1 << Maxn];
int f[1 << Maxn];
bool parity[1 << Maxn];
int main(void) {
scanf("%d%d", &n, &m);
nn = (1 << n) - 1;
for (int i = 0; i <= nn; ++i)
parity[i] = __builtin_parity(i);
memset(d, true, sizeof(d));
for (int i = 0; i < m; ++i) {
int u, v;
scanf("%d%d", &u, &v);
--u, --v;
e[u][v] = e[v][u] = true;
}
for (int s = 0; s <= nn; ++s) {
d[s] = true;
for (int i = 0; i < n; ++i) for (int j = i + 1; j < n; ++j)
if ((s >> i & 1) && (s >> j & 1) && e[i][j])
d[s] = false;
}
f[0] = 1;
for (int s = 1; s <= nn; ++s) {
for (int t = s; t; t = (t - 1) & s) {
if (d[t]) {
f[s] += (parity[t] ? f[s ^ t] : -f[s ^ t]);
if (f[s] < 0) f[s] += mod;
if (f[s] >= mod) f[s] -= mod;
}
}
}
int ans = f[nn];
ans = 1LL * ans * m % mod;
ans = 1LL * ans * (mod + 1) / 2 % mod;
printf("%d\n", ans);
exit(EXIT_SUCCESS);
} // main