题意:

给定一张图,选择一些点和一些边,使得割断任意没有选的边,被选中的点仍连通。对 \(10^9+7\) 取膜。

\(n \leq 5 \cdot 10^5\)

这篇题解会略讲缩边,详讲自己推 dp 状态与转移式的过程。

前置知识:桥(割边)、缩边。

感觉有些地方的逻辑绕了点,强制分了小段,希望能让阅读者看得更明白。


看到图和连通,立刻想到 tarjan。

割断一条没有选的边,选中的点仍连通,割非桥,整张图仍然连通。

把桥删掉,则整张图分成若干连通子图。可以把一张子图看成一个点,这样形成的新图上没有环,因为在环上的边都不是割边,那它就是一棵树。

设一个新点 \(i\) 中共有 \(cntp_{i}\) 个原图中的节点,\(cnte_{i}\) 条原图中的边。后面的点边均在新树上。

问题转化为:给定一棵树,每个点有 \((2^{cntp_i}-1) \cdot 2^{cnte_i}\) 种染色方式,每条边可以染色或不染色,问有几种染色方法,使得割断任意非染色边,所有染色点仍连通。

树上的问题相较图更好解决,因为转移方向固定为从儿子到父亲。那么显然,现在应该进行 dp。后面说的子树默认是根在 \(1\) 的情况。

什么在转移时会造成影响呢?考虑 \(u\) 和其儿子 \(v\)。当 \(v\) 子树中有点被染色时,这个点需要与 \(v\) 连通,且 \(v\) 需要与 \(u\) 连通,这是为了让 \(v\) 中的点与整棵树其余部分连通。否则,\(u\)\(v\) 间的边属于可有可无。

因此,设出 \(dp_{u,0/1}\)\(dp_{u,0}\) 强制 \(u\) 及其子树不选任意一个点(空);\(dp_{u,1}\) 强制 \(u\) 及其子树选至少一个点,且所有被选的点与 \(u\) 连通(不空)。

现在来考虑如何计算答案。

常用的做法是把一个点集放到它们的 lca 处计算,但是,\(dp_{u,1}\) 只考虑了连通性,而没有在意是否恰好是 lca。容斥应该可行。但是另一个想法是强制一个算入答案的状态不被另一个状态包含。

为了说明,先对树做一遍前缀和,设 \(sum_i\)\(i\) 及其子树中的边数和,包括被缩掉的边。容易看出 \(dp_{u,1} \cdot 2^{sum_1 – sum_u}\) 表示了所有点均位于 \(u\) 子树,且所有点与 \(u\) 连通时的情况数。

现在让一个情况不被另一个情况包含。注意到能统计到一个点集的点均在根到 lca 的链上,则统计时强制断掉 \(u\) 与其父亲间的连边,就不会出现这些点都与 lca 的某个父亲连通的情况,从而保证了一个情况不会被统计多次。

说人话,找到一个点集的 lca,顺着 lca 往根走,满足每一步走过的边都被染色,则我们将在走到的那个点上统计整个子集的贡献。

\[\left \{ \begin{aligned} ans & \gets dp_{u,1} \cdot 2^{sum_1 – sum_u – 1} \quad & u \not = 1 \\ ans & \gets dp_{u,1} & u = 1 \end{aligned} \right . \]

对答案的贡献说清楚了,接着来说 dp 的求法。

设现在位于 \(u\),要把 \(dp_{v,0/1}\) 合并进 \(u\),设 \(dp’_{u,0/1}\) 为没有合并入 \(v\) 前的情况。

\(dp_{u,0}\) 显然只能从 \(dp’_{u,0}\)\(dp_{v,0}\) 推出,且连接 \(u,v\) 的边可有可无。

\[dp_{u,0} \gets dp’_{u,0} \cdot 2dp_{v,0} \]

\(dp_{u,1}\) 可以从 \(dp’_{u,1}\) 处得到,此时 \(v\) 可以选择空或不空,空时中间边可有可无,否则必须有。

\[dp_{u,1} \gets dp’_{u,1} \cdot (dp_{v,1} + 2dp_{v,0}) \]

\(dp_{u,1}\) 也可以从 \(dp’_{u,0}\) 处得到,此时 \(v\) 必须非空,中间的边必须存在。

\[dp_{u,1} \gets dp’_{u,1} \cdot 2dp_{v,1} \]

初始时该点对应所有边均可选可不选,点看是否要求非空而言。

\[\left \{ \begin{aligned} dp_{u,0} & \gets 2^{cnte_u} \\ dp_{u,1} & \gets 2^{cnte_u} \cdot (2^{cntd_u} – 1) \end{aligned} \right . \]

结束。

AC 后去 infoj 翻了最短代码,深感自己写复杂了,居然 \(4\) 个 dfs……

#include <cstdio>
#include <iostream>
using namespace std;
const int M = 4e6, mod = 1000000007;
int add(int x, int y) {return ((x += y) >= mod) ? x-mod : x;}
void addn(int &x, int y) {if((x += y) >= mod) x -= mod;}
int mins(int x, int y) {return ((x -= y) < 0) ? x+mod : x;}
struct G{
    struct edge{
        int to, nxt, w;
    } e[M << 1];
    int head[M], cnt1 = 1;
    void link(int u, int v) {
        e[++cnt1] = {v, head[u], 1}; head[u] = cnt1;
    }
}g1, g2;
int fa[M]; 
int dfn[M], low[M], cnt; bool cut[M];
void tarjan(int u, int f) {
    low[u] = dfn[u] = ++cnt; 
    for(int i = g1.head[u]; i; i = g1.e[i].nxt) {
        int v = g1.e[i].to; if(v == f) continue;
        if(!dfn[v]){
            tarjan(v, u);
            low[u] = min(low[u], low[v]);
            if(low[v] > dfn[u]) g1.e[i].w = g1.e[i ^ 1].w = 0; // 0 标记为割边,i^1 用了成对变换的技巧,把反边一起标记
        }
        else low[u] = min(low[u], dfn[v]);
    }
} 
int siz1[M], siz2[M]; bool vis[M];
int bl[M];
int n, m, cnt2;
// 用于划分连通块,siz1 对应行文中的 cntp,点数;siz2 对应行文中的 cnte,边数
void dfs2(int u, int f, int t) {
    bl[u] = t; vis[u] = 1; ++siz1[t];
    for(int i = g1.head[u]; i; i = g1.e[i].nxt) {
        if(g1.e[i].w) ++siz2[t];
        int v = g1.e[i].to;
        if(g1.e[i].w == 0 || v == f) continue;
        if(!vis[v]) dfs2(v, u, t);
    }
}
int sm[M];
// 用于计算文中的 sum 数组,子树中边数和,包括被缩掉者
void dfs4(int u, int fa) {
    sm[u] = siz2[u];
    for(int i = g2.head[u]; i; i = g2.e[i].nxt) {
        int v = g2.e[i].to; if(v == fa) continue;
        dfs4(v, u);
        sm[u] += sm[v] + 1;
    }
}
int dp[M][2], pw[M], ans;
// 统计答案的 dfs,dp 意义如上文所叙
// dp_{u,0} 强制 u 及其子树不选任意一个点(空)
// dp_{u,1} 强制 u 及其子树选至少一个点,且所有被选的点与 u 连通(不空)。
void dfs3(int u, int fa) {
    dp[u][0] = pw[siz2[u]];
    dp[u][1] = 1ll * pw[siz2[u]] * (pw[siz1[u]] - 1) % mod;
    int tot = 0;
    for(int i = g2.head[u]; i; i = g2.e[i].nxt) {
        int v = g2.e[i].to; if(v == fa) continue;
        dfs3(v, u);
        dp[u][1] = add(1ll * dp[u][1] * add(1ll * dp[v][0] * 2 % mod, dp[v][1]) % mod, 1ll * dp[u][0] * dp[v][1] % mod);
        dp[u][0] = 1ll * dp[u][0] * 2 % mod * dp[v][0] % mod;
        addn(tot, dp[v][1]);
    }
    if(u != n+1) addn(ans, 1ll * dp[u][1] * pw[sm[n+1] - sm[u] - 1] % mod);
    else addn(ans, 1ll * dp[u][1] % mod);
}
int find(int u) {return u == fa[u] ? u : fa[u] = find(fa[u]);}
void merge(int u, int v) {if((u = find(u)) != (v = find(v))) fa[u] = v;}
int main(){
    scanf("%d %d", &n, &m);
    pw[0] = 1;
    for(int i = 1; i <= 2 * n + m; i++) pw[i] = 1ll * pw[i-1] * 2 % mod;
    for(int i = 1; i <= m; i++) {
        int u, v; scanf("%d %d", &u, &v);
        g1.link(u, v); g1.link(v, u);
    }
    tarjan(1, 0); cnt2 = n;
    for(int i = 1; i <= n; i++) if(!vis[i]) dfs2(i, 0, ++cnt2);
    for(int i = n+1; i <= 2*n; i++) {
        siz2[i] /= 2; 
    }
    // 缩边
    for(int i = 1; i <= 2*n; i++) fa[i] = i;
    for(int u = 1; u <= n; u++) {
        for(int i = g1.head[u]; i; i = g1.e[i].nxt) {
            int v = g1.e[i].to; 
            if(find(bl[u]) != find(bl[v])) g2.link(bl[u], bl[v]), g2.link(bl[v], bl[u]), merge(bl[u], bl[v]);
        }
    }
    // n+1 是缩完边后树的根
    dfs4(n+1, 0);
    dfs3(n+1, 0);
    printf("%d\n", ans);
}

感觉这是一道口嗨很简单的题,但细节什么的确实需要想清楚。评上位蓝或下位紫很恰当。

虽然我的思路看着很杂乱,但这确实是自己一步步想到的思路,自觉每一步的转化都能从上一步找到端倪,因为这是自己真实的思考过程。

赛时一眼缩边+树形 dp,然而没调完,花了 1h 回忆缩边的过程,回家再花了 30min 才写完。得到的教训是要好好复习学过的东西。

完结撒花 ovo

原文地址:http://www.cnblogs.com/purplevine/p/16930223.html

1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长! 2. 分享目的仅供大家学习和交流,请务用于商业用途! 3. 如果你也有好源码或者教程,可以到用户中心发布,分享有积分奖励和额外收入! 4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解! 5. 如有链接无法下载、失效或广告,请联系管理员处理! 6. 本站资源售价只是赞助,收取费用仅维持本站的日常运营所需! 7. 如遇到加密压缩包,默认解压密码为"gltf",如遇到无法解压的请联系管理员! 8. 因为资源和程序源码均为可复制品,所以不支持任何理由的退款兑现,请斟酌后支付下载 声明:如果标题没有注明"已测试"或者"测试可用"等字样的资源源码均未经过站长测试.特别注意没有标注的源码不保证任何可用性