UOJ#37 主旋律 题解

UOJ#37 主旋律 题解

题面在这里

简要题意

简明扼要的题♂意♂,求一个 n 个点、m条单向边的图中,有多少边集去掉后使得原图不强联通。 n15,0mn(n1)

题♂解♂

鉴于网上的题解都很神,跳过了好多好多好多关键部分,我这个退役选手只能自己写一份题♂解♂造福大众辣。

借鉴自陈老师的slides。

首先,原问题完全等价于求有多少边集保存下来使得原图强联通。然而这是一个非常非常复杂的问题,我们来考虑反面,有多少边集保存下来使得原图不强联通(给它一个奇妙的缩写叫不强连通数),最后用 2m 减去这个值就是答案。

方法一

由于不强联通,把图中的强连通分量缩点后,一定形成了一幅DAG。接下来我们来考虑一个Naive的做法,枚举所有可能的SCC缩点情况,DP计算DAG的个数。

f[S] 表示点集(缩过SCC后的一个映射,也就是说这个点集里不存在SCC)是 S 的情况下的DAG个数。因为一个DAG一定包含一些入度为0的点;同时已经有一个DAG时,如果再往里面加一些入度为 0 的点,并将这些新加的点向已有的点随意连边(单向的),所得的图还是DAG。我们来枚举这些入度为0的点,但是存在一些情况并不能恰好精准地枚举完(不能保证剩余的点中没有入度为 0 的点),因此采取容斥。

在枚举SCC后,可以得到这个式子:

f[S]=TS,T(1)|T|1×2cross[T][ST]×f[ST]

其中, cross[T][ST] 表示 T 集合中的点向ST集合中的点连的边数(单向的),由于可以随意连边而不破坏DAG,因此是 2cross[T][ST] 。但是……太慢啦!枚举SCC的复杂度窝不会算,反正很大就是了。这个DP的复杂度是 O(m3n) (包括了算 cross[T][ST] ),实在是太慢了。

方法二

稍加分析便会发现,上面这个做法是没必要枚举SCC的。在 T 中放了奇数个SCC的时候,上面的符号是+,偶数个是 ,再乘上SCC的划分方案数就可以了。

我们设T的一个划分方案是 AT=A1,A2,,Ak,Ai 是点的集合,满足 1i<jk,AiAj=,i=1kAi=T 。也就是说,我们只需要知道 k 而不需要具体地知道每个Ai,同时也避免了枚举SCC。上面的式子可以写成:

f[S]=TS,T1k|T|(1)k1×gk[T]×2cross[T][ST]×2h[ST]

其中 gk[T] 表示把 T 划分成k个SCC的方案数, h[S] 表示起点和终点都在 S 集合中的边的条数。

进一步,把所有的奇数k一起考虑,偶数也一起考虑,那么就可以写成:

f[S]=TS,Tk=1|T|+12g2k1[T]k=1|T|2g2k[T]2cross[T][ST]×2h[ST]

所以我们只要预处理出 k=1|T|+12g2k1[T]k=1|T|2g2k[T] cross[T][ST] 就可以辣!

定义 p[S]=k=1|S|+12g2k1[S]k=1|S|2g2k[S] dp[S] 表示集合 S 里的点组成一个SCC的方案数,也就是答案。

p[S]=TS,uTdp[T]×p[ST]+dp[S]

因为加入了一整个SCC之后奇偶性发生了翻转,要加上负号。始终保证 uT 是为了防止重复计数。

f[S]=TS,Tp[T]×2cross[T][ST]+h[ST]

dp[S]=2h[S]f[S]

这样就可以顺利DP辣。 cross[T][ST] 只要边枚举子集边从一个大一点的集合转移过来就可以了。时间复杂度 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陈老师。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值