题意简述
给定一个 \(n\) 个点 \(m\) 条边的有向简单图,问有多少种删边的方案,使得删去后整个图是强连通的,答案对 \(10^9+7\) 取模。
对于所有数据,\(1\leq n\leq 15\),\(0\leq m\leq n(n-1)\)。
题解
\(\text{Upd 2025/3/14}\):修改了一些笔误。
还是太神仙了。
强连通分量本身是比较难刻画的,但我们可以通过缩点刻画其结构性:若一张图是强连通的,那么它缩点后必然是一个单点,否则就是一个 DAG。DAG 计数是比较经典的,因此考虑正难则反,用 \(2^m\) 减去缩点后图是一个 DAG 的方案数。
先考虑一个比较简单的问题:求这个图中有多少个子图是一个 DAG。对于 DAG 计数,考虑拓扑排序,那么每次拓扑排序的 \(0\) 入度点对图进行了分层,形成了阶段性,很有利于我们进行 DP。令 \(f_S\) 表示 \(S\) 集合的导出子图中 DAG 子图的个数。枚举 \(0\) 入度点集合 \(T\):
其中 \(cnt(A,B)=\left|\{(u,v)|(u,v)\in E,u\in A,v\in B\}\right|\),即点集 \(A\) 中的点连向点集 \(B\) 中的点的边数。
但是,上面的转移方程是错误的,因为 \(T\rightarrow S\backslash T\) 的边是乱连的,我们无法保证 \(S\) 恰好是 \(0\) 入度点集,会算重。而 \(2^{cnt(T,S\backslash T)}f_{S\backslash T}\) 计算的实际上是钦定 \(T\) 为 \(0\) 入度点集的方案数,因此考虑容斥。令 \(f_{T,S}\) 表示 \(S\) 点集中 \(T\) 恰好 是 \(0\) 入度点集的方案数,\(g_{T,S}\) 表示 \(S\) 中钦定 \(T\) 为 \(0\) 入度点集的方案数。那么可以得到
根据子集反演,
代回再变换求和顺序,可以得到
这样就解决了 DAG 子图计数问题,同时我们也得到了一个显然的暴力:搜出缩点的方案,然后跑 DAG 子图计数。
考虑怎么优化。我们的暴力是形如搜出缩点方案 \(V=\bigcup_{i=1}^kS_i\),然后答案就是
变换求和顺序,我们先去枚举 \(T\),即缩点后的零入度点的并集在原图上对应的点集,注意到容斥系数只和零入度点划分成的 SCC 数量有关,并且对于 \(S\backslash T\) 中的点,任意的子图都是合法的。令 \(h_{k,T}\) 表示将 \(T\) 中的点划分成 \(k\) 个互不相连的 SCC 的方案数,那么容易得到
进一步地,容斥系数只跟零入度点划分成的 SCC 数量奇偶性有关,奇数个贡献为正,偶数个贡献为负,容易想到令 \(h_T\) 表示把 \(T\) 划分成奇数个 SCC 的方案数减去划分成偶数个 SCC 的方案数,转移方程变为
再来考虑 \(h_S\) 的转移。容易想到枚举某个子集 \(T\subseteq S\) 作为其中一个 SCC,但显然会重复,于是我们对其添加限制,改为枚举 \(\operatorname{lowbit}(S)\) 对应点 \(p\) 所在的 SCC,容易得到转移方程:
加上 \(ans_S\) 表示将 \(S\) 划分为一整个 SCC,\(\sum\) 前面的负号是因为多出了 \(T\) 这个 SCC,奇偶性改变。
但是这样似乎 \(h\) 和 \(ans\) 似乎会相互转移啊?我们仔细思考,\(ans\) 转移时只会在 \(T=S\) 的时候用到 \(h_S\),而这个的实际意义是 \(S\) 就是一个强连通分量,但 \(\sum\) 处要计算的是不合法的方案数,所以不应该被包含进来,这样就恰好不会相互转移了。也就是说,我们先计算 \(h_S-ans_S\) 的部分,然后计算\(ans_S\),最后给 \(h_S\) 加回 \(ans_S\) 就行了。可以结合代码理解。
时间复杂度是 \(O(3^nn^2)\) 的,无法通过。瓶颈在于计算 \(cnt(T,S\backslash T)\)。暴力计算无法承受,那么考虑固定 \(S\),假设我们得到了较大的 \(T\) 对应的 \(cnt(T,S\backslash T)\),然后尝试递推出当前的 \(cnt(T,S\backslash T)\),这就很简单了,考虑 \(\operatorname{lowbit}(S\backslash T)\) 对应的点 \(p\),那么
而形如 \(cnt(p,T)\) 或者 \(cnt(T,p)\) 的单点到集合的边数显然可以 \(O(2^nn^2)\) 预处理出来,于是我们就可以在枚举 \(S\) 的过程中顺便把所有的 \(cnt(T,S\backslash T)\) 递推出来。总体时间复杂度为 \(O(3^n)\)。
代码
int n, m, mat[N][N];
int pw2[M], pc[S], ocnt[N][S], icnt[N][S], cnt[S], f[S], g[S];
int tcnt[S];int main() {ios::sync_with_stdio(false), cin.tie(nullptr);cin >> n >> m;for (int i = 1, u, v; i <= m; ++i) cin >> u >> v, mat[--u][--v] = 1;pw2[0] = 1;for (int i = 1; i <= m; ++i) pw2[i] = (pw2[i - 1] << 1) % MOD;for (int i = 0; i < n; ++i) for (int s = 0; s < 1 << n; ++s)for (int j = 0; j < n; ++j)if (s >> j & 1) ocnt[i][s] += mat[i][j], icnt[i][s] += mat[j][i];for (int s = 1; s < 1 << n; ++s) {int lb = lowbit(s), i = log2(lb), ss = s ^ lb;cnt[s] = cnt[ss] + icnt[i][ss] + ocnt[i][ss];pc[s] = pc[s - lowbit(s)] + 1;}for (int s = 1; s < 1 << n; ++s) {int lb = lowbit(s);for (int t = (s - 1) & s; t; t = (t - 1) & s)if (t & lb) g[s] = (g[s] - 1ll * f[t] * g[s ^ t] % MOD) % MOD;tcnt[s] = 0, f[s] = pw2[cnt[s]];for (int t = s; t; t = (t - 1) & s) {int lb = lowbit(s ^ t), i = log2(lb);if (t != s) tcnt[t] = tcnt[t | lb] - ocnt[i][s ^ t ^ lb] + icnt[i][t];else tcnt[t] = 0;f[s] = (f[s] - 1ll * g[t] * pw2[tcnt[t]] % MOD * pw2[cnt[s ^ t]] % MOD) % MOD;}g[s] = (g[s] + f[s]) % MOD;}cout << (f[(1 << n) - 1] + MOD) % MOD;return 0;
}