题解:P11714 [清华集训 2014] 主旋律

题意简述

给定一个 \(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\)

\[f_S=\sum_{\substack {T\subseteq S\\T\neq\varnothing}}2^{cnt(T,S\backslash T)}f_{S\backslash 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\) 入度点集的方案数。那么可以得到

\[g_{T,S}=2^{cnt(T,S\backslash T)}f_{S\backslash T}=\sum_{T\subseteq R\subseteq S}f_{R,S} \]

根据子集反演,

\[f_{T,S}=\sum_{T\subseteq R\subseteq S}(-1)^{|R|-|T|}g_{T,S} \]

代回再变换求和顺序,可以得到

\[\begin{align*} f_S & =\sum_{\substack {T\subseteq S\\T\neq\varnothing}}f_{T,S} \\ & =\sum_{\substack {T\subseteq S\\T\neq\varnothing}}\sum_{T\subseteq R\subseteq S}(-1)^{|R|-|T|}2^{cnt(R,S\backslash R)}f_{S\backslash R} \\ & =\sum_{R\subseteq S}(-1)^{|R|}2^{cnt(R,S\backslash R)}f_{S\backslash R}\sum_{\substack {T\subseteq R\\T\neq\varnothing}}(-1)^{|T|} \\ & =\sum_{R\subseteq S}(-1)^{|R|+1}2^{cnt(R,S\backslash R)}f_{S\backslash R} \end{align*} \]

这样就解决了 DAG 子图计数问题,同时我们也得到了一个显然的暴力:搜出缩点的方案,然后跑 DAG 子图计数。

考虑怎么优化。我们的暴力是形如搜出缩点方案 \(V=\bigcup_{i=1}^kS_i\),然后答案就是

\[ans_S=2^{cnt(S,S)}-\sum_{S_1,\cdots,S_k}\left(\prod_{i=1}^kans_{S_i}\right)\sum_{\substack{T\subseteq \{S_1,\cdots,S_k\}\\T\neq\varnothing}}(-1)^{|T|+1}2^{cnt(T,S\backslash T)}f_{S\backslash T} \]

变换求和顺序,我们先去枚举 \(T\),即缩点后的零入度点的并集在原图上对应的点集,注意到容斥系数只和零入度点划分成的 SCC 数量有关,并且对于 \(S\backslash T\) 中的点,任意的子图都是合法的。令 \(h_{k,T}\) 表示将 \(T\) 中的点划分成 \(k\) 个互不相连的 SCC 的方案数,那么容易得到

\[ans_S=2^{cnt(S,S)}-\sum_{\substack {T\subseteq S\\T\neq\varnothing}}\sum_{k=1}^{|T|}(-1)^{k+1}h_{k,T}2^{cnt(T,S\backslash T)}2^{cnt(S\backslash T,S\backslash T)} \]

进一步地,容斥系数只跟零入度点划分成的 SCC 数量奇偶性有关,奇数个贡献为正,偶数个贡献为负,容易想到令 \(h_T\) 表示把 \(T\) 划分成奇数个 SCC 的方案数减去划分成偶数个 SCC 的方案数,转移方程变为

\[ans_S=2^{cnt(S,S)}-\sum_{\substack {T\subseteq S\\T\neq\varnothing}}h_T2^{cnt(T,S\backslash T)}2^{cnt(S\backslash T,S\backslash T)} \]

再来考虑 \(h_S\) 的转移。容易想到枚举某个子集 \(T\subseteq S\) 作为其中一个 SCC,但显然会重复,于是我们对其添加限制,改为枚举 \(\operatorname{lowbit}(S)\) 对应点 \(p\) 所在的 SCC,容易得到转移方程:

\[h_S=ans_S-\sum_{\substack{T\subset S\\p\in T}}ans_Th_{S\backslash T} \]

加上 \(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(T,S\backslash T)=cnt(T\cup\{p\},S\backslash T\backslash \{p\})-cnt(p,S\backslash T\backslash \{p\})+cnt(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;
}
posted @ 2025-03-19 12:41  P2441M  阅读(44)  评论(0)    收藏  举报