题意简述
给你 \(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 XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%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 XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%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];elsef[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 XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%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]];elsef[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 XuYuemingfreopen("obelisk.in", "r", stdin);freopen("obelisk.out", "w", stdout);
#endifscanf("%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;
}