巨神兵 题解
题意简述
给你 \(n\) 个点 \(m\) 条边的有向图,请问在 \(2^m\) 种选择边的方式中,有多少种方式不会成环?对 \(10^9+7\) 取模。
\(n\leq17,m\leq n\cdot(n-1)\)。
题目分析
看到数据范围这么小,应该是状压了。我们计数对象是边,状态记录的却是某些点怎么样了,怎么设计状态并转移呢?
考虑怎么刻画一个 DAG。我们判断 DAG 使用拓扑排序,而它正是一种对于点的过程,那么尝试从这里下手。它将我们的点分成了若干组,第一组是入度为 \(0\) 的点构成的集合,第二组是去掉第一组和所连的边后入度为 \(0\) 的点,以此类推。那我们就考虑一组一组地加点。
不妨我们考虑往第一组之前加一组点,即新加的一组点中,连出若干条边至原先的第一组点。往最后一组之后再加一组,和在第一组之前再加一组是相同的:考虑将边反向,成环状态不变,而此时第一组变成了最后一组,最后一组变成了第一组。
我们设 \(f_{S,T}\) 表示点集为 \(S\) 的子图,其中第一组是 \(T\) 的方案数。只有 \(T \subseteq S\) 的状态才是有效的,事实上,三进制 DP 是可行的,如果愿意写的话。初始值 \(f_{S,S}=1\)。答案即为 \(\sum\limits_{T}f_{\{i\}_{i=1}^n,T}\)。
转移的时候,枚举新的第一层 \(T'\) 且 \(T'\cap S=\varnothing\)。将 \(T'\) 连出的边分为连向 \(T\) 和连向 \(S\setminus T\) 讨论。对于一个 \(u\in T\),它必须要至少有一个入度,假设有 \(c_1\) 个 \(T'\) 中的点连向它,那么方案数为 \(2^{c_1}-1\);对于一个 \(u\in T'\),连向 \(S\setminus T\) 的边数设为 \(c_2\),那么随便连,方案数为 \(2^{c_2}\)。
于是我们得到了 \(\mathcal{O}(n\cdot4^n + m)\) 的做法,考虑优化。尝试将状态简化为 \(\mathcal{O}(2^n)\),记 \(f_S\) 表示点集为 \(S\) 的方案数。
类似枚举新的第一层 \(T\) 满足 \(T\cap S=\varnothing\),再记 \(c_1\) 表示 \(T\) 中的点连向 \(S\) 中的点的边数。可能我们会列出一个一看就不对的转移 \(f_{S\cup T}\Leftarrow f_S\cdot 2^{c_1}\)(其中 \(a\Leftarrow b\) 表示 \(a\gets a+b\))。这样并不能保证 \(S\) 中的第一层都会有入度,从而导致了 \(S\cup T\) 的第一层为 \(T\) 并上原先 \(S\) 中第一层的一部分。这显然会导致重复,对于某一个局面,它的第一组会被多种加点方式统计到。我们接下来要把这些重复容斥掉。
注意到,容斥系数为 \((-1)^{|T|+1}\),即 \(f_{S\cup T}\Leftarrow (-1)^{|T|+1}\cdot2^{c_1}\cdot f_S\) 即为正确的转移。
正确性证明:
设 \(w_{T\Rightarrow S}\) 表示 \(T\) 中的点连向 \(S\) 中的点的边数。设 \(g_{S,T}\) 表示点集为 \(S\) 的子图,其中第一组是 \(T\) 的方案数。再设 \(h_{S,T}\) 表示点集为 \(S\) 的子图,其中第一组至少为 \(T\) 的方案数。
根据容斥,我们有 \(g_{S,T}=\sum\limits_{T\subseteq T'\subseteq S}(-1)^{|T'|-|T|}h_{S,T'}\)。而 \(h_{S,T} = f_{S\setminus T}\cdot 2^{w_{T\Rightarrow S\setminus T}}\)(考虑到 \(f\) 中囊括了第一组 \(\subseteq S\setminus T\) 的所有情况,连边后的第一组 \(T'\) 囊括了 \(T\subseteq T'\subseteq S\) 的所有情况)。所以:
发现这就是我们的转移方程,而容斥系数即为 \((-1)^{|S\setminus T|+1}\)。
\(\mathcal{O}(3^n)\) 枚举 \(S\) 和 \(T\),再 \(\mathcal{O}(n)\) 地计算 \(w_{T\Rightarrow S}\) 就能做到 \(\mathcal{O}(n\cdot 3^n + m)\)。发现在 \(S\) 固定时,枚举 \(T\) 的同时,可以轻松递推维护 \(w\)。对于从小到大枚举的 \(T\),取一 \(x\in T\),\(w_{T\setminus\{x\}\Rightarrow S}\) 已经计算得出,那么加上 \(w_{\{x\}\Rightarrow S}\) 就是 \(w_{T\Rightarrow S}\)。于是可以做到 \(\mathcal{O}(3^n + m)\)。
代码
$\mathcal{O}(n\cdot4^n + m)$
#include <cstdio>
#include <iostream>
#include <limits>
#include <cassert>
using namespace std;
using namespace Mod_Int_Class;
const int N = 13, M = 1 << 10 | 520;
int n, m, e[N], r[N];
mint f[M][M], pw[N * N];
signed main() {
#ifndef XuYueming
freopen("obelisk.in", "r", stdin);
freopen("obelisk.out", "w", stdout);
#endif
scanf("%d%d", &n, &m);
pw[0] = 1;
for (int i = 1; i <= m; ++i)
pw[i] = pw[i - 1] + pw[i - 1];
for (int u, v, i = 1; i <= m; ++i) {
scanf("%d%d", &u, &v);
e[u - 1] |= 1 << (v - 1);
r[v - 1] |= 1 << (u - 1);
}
for (int s = 0; s < 1 << n; ++s)
f[s][s] = 1;
for (int s = 0; s < 1 << n; ++s) {
for (int t = s; t; t = (t - 1) & s) {
int S = ((1 << n) - 1) ^ s;
for (int ss = S; ss; ss = (ss - 1) & S) {
mint res = 1;
for (int i = 0; i < n; ++i)
if (t & 1 << i) {
int x1 = __builtin_popcount(r[i] & ss);
res *= pw[x1] - 1;
}
for (int i = 0; i < n; ++i)
if (ss & 1 << i) {
int x2 = __builtin_popcount(e[i] & (s ^ t));
res *= pw[x2];
}
f[s | ss][ss] += f[s][t] * res;
}
}
}
mint ans = 0;
for (int s = 0; s < 1 << n; ++s)
ans += f[(1 << n) - 1][s];
printf("%d", ans.raw());
return 0;
}
$\mathcal{O}(n\cdot 3^n + m)$
#include <cstdio>
#include <iostream>
#include <limits>
#include <cassert>
using namespace std;
using namespace Mod_Int_Class;
const int N = 18;
int n, m, e[N];
mint f[1 << N | 10], pw[N * N];
signed main() {
#ifndef XuYueming
freopen("obelisk.in", "r", stdin);
freopen("obelisk.out", "w", stdout);
#endif
scanf("%d%d", &n, &m);
pw[0] = 1;
for (int i = 1; i <= m; ++i)
pw[i] = pw[i - 1] + pw[i - 1];
for (int u, v; m--; )
scanf("%d%d", &u, &v), e[u - 1] |= 1 << (v - 1);
f[0] = 1;
for (int st = 0; st < 1 << n; ++st) {
int S = ((1 << n) - 1) ^ st;
for (int T = S; T; T = (T - 1) & S) {
int cnt = 0;
for (int i = 0; i < n; ++i)
if (T & 1 << i)
cnt += __builtin_popcount(e[i] & st);
if (__builtin_popcount(T) & 1)
f[st | T] += f[st] * pw[cnt];
else
f[st | T] -= f[st] * pw[cnt];
}
}
printf("%d", f[(1 << n) - 1].raw());
return 0;
}
$\mathcal{O}(3^n + m)$
#include <cstdio>
#include <iostream>
#include <limits>
#include <cassert>
using namespace std;
using namespace Mod_Int_Class;
const int N = 18;
int n, m, e[1 << N | 10];
mint f[1 << N | 10], pw[N * N];
int ccc[1 << N | 10], popc[1 << N | 10];
signed main() {
#ifndef XuYueming
freopen("obelisk.in", "r", stdin);
freopen("obelisk.out", "w", stdout);
#endif
scanf("%d%d", &n, &m);
pw[0] = 1;
for (int i = 1; i <= m; ++i)
pw[i] = pw[i - 1] + pw[i - 1];
for (int s = 1; s < 1 << n; ++s)
popc[s] = popc[s >> 1] + (s & 1);
for (int u, v, i = 1; i <= m; ++i)
scanf("%d%d", &u, &v), e[1 << (u - 1)] |= 1 << (v - 1);
f[0] = 1;
for (int st = 0; st < 1 << n; ++st) {
for (int T = (st + 1) | st; T < 1 << n; T = (T + 1) | st) {
int S = T ^ st;
ccc[S] = ccc[S & (S - 1)] + popc[e[S & -S] & st];
if (popc[S] & 1)
f[T] += f[st] * pw[ccc[S]];
else
f[T] -= f[st] * pw[ccc[S]];
}
}
printf("%d", f[(1 << n) - 1].raw());
return 0;
}
卡常后
#include <cstdio>
const int mod = 1e9 + 7;
const int N = 18, M = 1 << 17 | 520;
int n, m, e[M];
int f[M], pw[N * N];
int ccc[M], popc[M];
signed main() {
#ifndef XuYueming
freopen("obelisk.in", "r", stdin);
freopen("obelisk.out", "w", stdout);
#endif
scanf("%d%d", &n, &m);
pw[0] = 1;
for (int i = 1, lst = 1; i <= m; ++i) {
lst <<= 1;
lst = lst >= mod ? lst - mod : lst;
pw[i] = lst;
}
int U = (1 << n) - 1;
for (int s = 1; s <= U; ++s)
popc[s] = popc[s >> 1] + (s & 1);
for (int u, v, i = 1; i <= m; ++i)
scanf("%d%d", &u, &v), e[1 << (u - 1)] |= 1 << (v - 1);
f[0] = 1;
for (int st = 0; st < U; ++st) {
for (int T = (st + 1) | st; T <= U; T = (T + 1) | st) {
int S = T ^ st;
int x = ccc[S & (S - 1)] + popc[e[S & -S] & st];
ccc[S] = x;
if (popc[S] & 1) {
f[T] += 1ll * f[st] * pw[x] % mod;
f[T] = f[T] >= mod ? f[T] - mod : f[T];
} else {
f[T] -= 1ll * f[st] * pw[x] % mod;
f[T] = f[T] < 0 ? f[T] + mod : f[T];
}
}
}
printf("%d", f[U]);
return 0;
}
本文作者:XuYueming,转载请注明原文链接:https://www.cnblogs.com/XuYueming/p/18759987。
若未作特殊说明,本作品采用 知识共享署名-非商业性使用 4.0 国际许可协议 进行许可。

浙公网安备 33010602011771号