UOJ#37 主旋律 题解
简要题意
简明扼要的题♂意♂,求一个$n$个点、$m$条单向边的图中,有多少边集去掉后使得原图不强联通。$n \leqslant 15,0 \leqslant m \leqslant n(n−1)$。
题♂解♂
鉴于网上的题解都很神,跳过了好多好多好多关键部分,我这个退役选手只能自己写一份题♂解♂造福大众辣。
借鉴自陈老师的slides。
首先,原问题完全等价于求有多少边集保存下来使得原图强联通。然而这是一个非常非常复杂的问题,我们来考虑反面,有多少边集保存下来使得原图不强联通(给它一个奇妙的缩写叫不强连通数),最后用$2^m$减去这个值就是答案。
方法一
由于不强联通,把图中的强连通分量缩点后,一定形成了一幅DAG。接下来我们来考虑一个Naive的做法,枚举所有可能的SCC缩点情况,DP计算DAG的个数。
设$f[S]$表示点集(缩过SCC后的一个映射,也就是说这个点集里不存在SCC)是$S$的情况下的DAG个数。因为一个DAG一定包含一些入度为$0$的点;同时已经有一个DAG时,如果再往里面加一些入度为$0$的点,并将这些新加的点向已有的点随意连边(单向的),所得的图还是DAG。我们来枚举这些入度为$0$的点,但是存在一些情况并不能恰好精准地枚举完(不能保证剩余的点中没有入度为$0$的点),因此采取容斥。
在枚举SCC后,可以得到这个式子:
$\displaystyle f[S]=\sum_{T \subset S, T \not = \varnothing} (-1)^{|T|-1} \times 2^{cross[T][S-T]} \times f[S-T]$
其中,$cross[T][S-T]$表示$T$集合中的点向$S-T$集合中的点连的边数(单向的),由于可以随意连边而不破坏DAG,因此是$2^{cross[T][S-T]}$。但是……太慢啦!枚举SCC的复杂度窝不会算,反正很大就是了。这个DP的复杂度是$\mathcal{O}(m3^n)$(包括了算$cross[T][S-T]$),实在是太慢了。
方法二
稍加分析便会发现,上面这个做法是没必要枚举SCC的。在$T$中放了奇数个SCC的时候,上面的符号是$+$,偶数个是$-$,再乘上SCC的划分方案数就可以了。
我们设$T$的一个划分方案是$A_T={A_1,A_2,\ldots,A_k},A_i$是点的集合,满足$\displaystyle \forall_{1 \leqslant i < j \leqslant k}, A_i \bigcap A_j=\varnothing, \bigcup_{i=1}^k A_i=T$。也就是说,我们只需要知道$k$而不需要具体地知道每个$A_i$,同时也避免了枚举SCC。上面的式子可以写成:
$\displaystyle f[S]=\sum_{T \subset S, T \not = \varnothing} \sum_{1 \leqslant k \leqslant |T|} (-1)^{k-1} \times g_k[T] \times 2^{cross[T][S-T]} \times 2^{h[S-T]}$
其中$g_k[T]$表示把$T$划分成$k$个SCC的方案数,$h[S]$表示起点和终点都在$S$集合中的边的条数。
进一步,把所有的奇数$k$一起考虑,偶数也一起考虑,那么就可以写成:
$\displaystyle f[S]=\sum_{T \subset S, T \not = \varnothing} \left(\sum_{k=1}^{\lfloor \frac{|T|+1}{2} \rfloor}g_{2k-1}[T]-\sum_{k=1}^{\lfloor \frac{|T|}{2} \rfloor} g_{2k}[T]\right)2^{cross[T][S-T]} \times 2^{h[S-T]}$
所以我们只要预处理出$\displaystyle \sum_{k=1}^{\lfloor \frac{|T|+1}{2} \rfloor}g_{2k-1}[T]-\sum_{k=1}^{\lfloor \frac{|T|}{2} \rfloor} g_{2k}[T]$和$cross[T][S-T]$就可以辣!
定义$\displaystyle p[S] = \sum_{k=1}^{\lfloor \frac{|S|+1}{2} \rfloor}g_{2k-1}[S]-\sum_{k=1}^{\lfloor \frac{|S|}{2} \rfloor} g_{2k}[S]$,$dp[S]$表示集合$S$里的点组成一个SCC的方案数,也就是答案。
$\displaystyle p[S] = -\sum_{T \subset S, u \in T}dp[T] \times p[S - T] + dp[S]$
因为加入了一整个SCC之后奇偶性发生了翻转,要加上负号。始终保证$u \in T$是为了防止重复计数。
$\displaystyle f[S]=\sum_{T \subset S, T \not = \varnothing} p[T] \times 2^{cross[T][S-T]+h[S-T]}$
$dp[S] = 2^{h[S]}-f[S]$
这样就可以顺利DP辣。$cross[T][S-T]$只要边枚举子集边从一个大一点的集合转移过来就可以了。时间复杂度$\mathcal{O}(3^n)$。
代码(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陈老师。