UOJ#37 主旋律 题解
简要题意
简明扼要的题♂意♂,求一个
n
个点、
题♂解♂
鉴于网上的题解都很神,跳过了好多好多好多关键部分,我这个退役选手只能自己写一份题♂解♂造福大众辣。
借鉴自陈老师的slides。
首先,原问题完全等价于求有多少边集保存下来使得原图强联通。然而这是一个非常非常复杂的问题,我们来考虑反面,有多少边集保存下来使得原图不强联通(给它一个奇妙的缩写叫不强连通数),最后用 2m 减去这个值就是答案。
方法一
由于不强联通,把图中的强连通分量缩点后,一定形成了一幅DAG。接下来我们来考虑一个Naive的做法,枚举所有可能的SCC缩点情况,DP计算DAG的个数。
设
f[S]
表示点集(缩过SCC后的一个映射,也就是说这个点集里不存在SCC)是
S
的情况下的DAG个数。因为一个DAG一定包含一些入度为
在枚举SCC后,可以得到这个式子:
f[S]=∑T⊂S,T≠∅(−1)|T|−1×2cross[T][S−T]×f[S−T] 其中, cross[T][S−T] 表示 T 集合中的点向
S−T 集合中的点连的边数(单向的),由于可以随意连边而不破坏DAG,因此是 2cross[T][S−T] 。但是……太慢啦!枚举SCC的复杂度窝不会算,反正很大就是了。这个DP的复杂度是 O(m3n) (包括了算 cross[T][S−T] ),实在是太慢了。方法二
稍加分析便会发现,上面这个做法是没必要枚举SCC的。在 T 中放了奇数个SCC的时候,上面的符号是
+ ,偶数个是 − ,再乘上SCC的划分方案数就可以了。我们设
T 的一个划分方案是 AT=A1,A2,…,Ak,Ai 是点的集合,满足 ∀1⩽i<j⩽k,Ai⋂Aj=∅,⋃i=1kAi=T 。也就是说,我们只需要知道 k 而不需要具体地知道每个Ai ,同时也避免了枚举SCC。上面的式子可以写成:f[S]=∑T⊂S,T≠∅∑1⩽k⩽|T|(−1)k−1×gk[T]×2cross[T][S−T]×2h[S−T]
其中 gk[T] 表示把 T 划分成
k 个SCC的方案数, h[S] 表示起点和终点都在 S 集合中的边的条数。进一步,把所有的奇数
k 一起考虑,偶数也一起考虑,那么就可以写成:f[S]=∑T⊂S,T≠∅⎛⎝⎜∑k=1⌊|T|+12⌋g2k−1[T]−∑k=1⌊|T|2⌋g2k[T]⎞⎠⎟2cross[T][S−T]×2h[S−T]
所以我们只要预处理出 ∑k=1⌊|T|+12⌋g2k−1[T]−∑k=1⌊|T|2⌋g2k[T] 和 cross[T][S−T] 就可以辣!
定义 p[S]=∑k=1⌊|S|+12⌋g2k−1[S]−∑k=1⌊|S|2⌋g2k[S] , dp[S] 表示集合 S 里的点组成一个SCC的方案数,也就是答案。
p[S]=−∑T⊂S,u∈Tdp[T]×p[S−T]+dp[S] 因为加入了一整个SCC之后奇偶性发生了翻转,要加上负号。始终保证 u∈T 是为了防止重复计数。
f[S]=∑T⊂S,T≠∅p[T]×2cross[T][S−T]+h[S−T]
dp[S]=2h[S]−f[S]
这样就可以顺利DP辣。 cross[T][S−T] 只要边枚举子集边从一个大一点的集合转移过来就可以了。时间复杂度 O(3n) 。
代码(C++)
#include <bits/stdc++.h> using namespace std; typedef long long ll; const ll pm = 1000000007; const int N = 15; int out[N], in[N], fun[1 << N], pw[N * N + 5], cnt[1 << N], cross[1 << N]; ll dp[1 << N], all[1 << N], p[1 << N]; int main() { int n, m, tn, i, a, b, S, T, rem, t, tmp; cin >> n >> m; tn = (1 << n) - 1; for (i = 1; i <= m; ++i) { cin >> a >> b; --a; --b; out[a] |= 1 << b; in[b] |= 1 << a; } for (i = 0; i < n; ++i) fun[1 << i] = i; for (pw[0] = i = 1; i <= n * n; ++i) pw[i] = pw[i - 1] * 2 % pm; for (S = 1; S <= tn; ++S) cnt[S] = cnt[S >> 1] + (S & 1); dp[0] = all[0] = 1; p[0] = pm - 1; for (S = 1; S <= tn; ++S) { if (cnt[S] == 1) {dp[S] = p[S] = all[S] = 1; continue;} for (i = tmp = 0; i < n; ++i) if (S & (1 << i)) tmp += cnt[S & out[i]]; all[S] = dp[S] = pw[tmp]; rem = S - (S & -S); for (T = rem; ; T = (T - 1) & rem) { t = T | (S & -S); if (t < S) (p[S] += pm - dp[t] * p[S - t] % pm) %= pm; if (!T) break; } for (T = S; T; T = (T - 1) & S) { if (T == S) cross[T] = 0; else { t = fun[(S - T) & (T - S)]; cross[T] = cross[T + (1 << t)] + cnt[in[t] & T] - cnt[out[t] & (S - T - (1 << t))]; } (dp[S] += pm - p[T] * all[S - T] % pm * pw[cross[T]] % pm) %= pm; } (p[S] += dp[S]) %= pm; } cout << dp[tn] << endl; return 0; }
完结撒花!
无限ym陈老师。