「NOIP2022」建造军营 题解

前言

题目链接:洛谷

题意简述

yzh 送你一张 \(n\) 个点 \(m\) 条边的无向连通图,你可以决定选择 \(n\) 个点中若干个、\(m\) 条边中若干条,方案数为 \(2^n2^m\)。在你操作后,yzh 会任意挑选一条边,如果这条边没有被你选中,那么就要断开这条边,否则什么事也没发生。你需要保证无论 yzh 怎么选择,你选出的点集在操作后是连通的。求你选择的方案数,对 \(998244353\) 取模。

\(1 \leq n \leq 5 \times 10^5\)\(n - 1 \leq m \leq 10^6\),保证图连通,无自环、重边。

题目分析

这么大一张无向图,要么考虑 tarjan 缩点,要么考虑随便建出一棵 DFS 树,分为树边、非树边考虑。对于此题,两种做法均可,作者仅介绍更为好像的 tarjan 缩点做法,DFS 树方法留给读者思考。

发现,yzh 选择的边会导致原图不连通,当且仅当选择了图上的桥。用 tarjan 把边双缩点后,图变成了一棵树。问题此时转变成了,在 \(n'\) 点中选出一个子集 \(S\),若选择了 \(u \in S\),在原问题上有 \(2^{\operatorname{siz}(u)} - 1\) 中选择方案(\(\operatorname{siz}(u)\) 表示双连通分量 \(u\) 在原图中点数),表示 \(u\) 这个边双中选择了若干个不为空的点;记 \(S\) 构成的虚树(即最小的包含 \(S\) 的连通块)中有 \(k\) 条边,那么我们必须选择这 \(k\) 条边,才能保证 \(S\) 连通,而剩下 \((n' - 1) - k\) 和点双中的 \(m - (n' - 1)\) 条,共 \(m - k\) 条边随便选择,方案数为 \(2^{m - k}\)

形式化地表示如下:

\[\large ans = \sum _ {S \subseteq \{i\}_{i = 1}^n} 2^{m - \operatorname{calc}(S)} \prod _ {u \in S} (2^{\operatorname{siz}(u)} - 1) \]

这样一坨东西应该需要树形 DP 解决。

\(S\) 计数显然不好算,考虑从最小连通块角度切入。我们枚举这个最小连通块,连通块最外层的点(即度为 \(1\) 的点)\(u\) 必须至少选择了 \(\operatorname{siz}(u)\) 中的一个,方案数为 \(2^{\operatorname{siz}(u)} - 1\),而内部节点可以随便选择,方案数为 \(2^{\operatorname{siz}(u)}\)。我们此时不用纠结最小连通块的边数 \(\operatorname{calc}(S)\) 为多少了,只需要在 DP 的时候,如果选择了某一条边到连通块中,乘上 \(2^{-1}\),最后的答案再乘以 \(2^m\) 即可。这样枚举能够包含原先所有情况。

那么 DP 的状态也很容易想了,用 \(f_{u, 0/1/2}\) 分别表示 \(u\) 为某一个连通块最浅的点,其有 \(0\) 个儿子、恰 \(1\) 个儿子、\(2\) 个及以上儿子的答案。拥有 \(0\) 个儿子的、或者深度最浅并且恰有 \(1\) 个儿子的结点,即为最外层的点。

\(f_{u, 0}\) 最简单,不需要转移,为 \(2^{\operatorname{siz}(u)} - 1\)

\(f_{u, 1}\) 继承某一个儿子的答案,乘上 \(u\) 的方案数,根据加法原理累加即可。注意选中了 \(u\) 和其儿子间的一条边,需要乘上 \(2^{-1}\) 的系数。为了方便,我们记 \(g_u = f_{u, 0} + f_{u, 1} + f_{u, 2}\)

\[f_{u, 1} = 2^{\operatorname{siz}(u)} \cdot 2^{-1} \sum _ {v \in \operatorname{son}(u)} g_v \]

\(f_{u, 2}\) 直接来不好做,需要和 \(f_{u, 1}\) 一起算。我们顺次枚举孩子 \(v\),先让 \(f_{u, 2} \gets f_{u, 2} + (f_{u, 1} + f_{u, 2}) \cdot 2^{-1} \cdot g_v\),再 \(f_{u, 1} \gets f_{u, 1} + 2^{\operatorname{siz}(u)} \cdot 2^{-1} \cdot g_v\)。前者表示讨论是否选择 \((u, v)\) 再根据加法原理累加。

如何统计答案呢?我们考虑在联通块最浅的那个点统计。\(f_{u, 0 / 2}\) 显然直接累加即可,但是 \(f_{u, 1}\) 需要保证 \(u\) 至少选定了一个点,所以需要加上 \(f_{u, 1}\) 后减去每个非根的 \(u\)\(2^{-1} g_u\)

此时树形 DP 时间复杂度已经是 \(\mathcal{O}(n)\) 的了,总的时间复杂度为 \(\mathcal{O}(n + m)\) 足以通过本题。不过在写题解的过程中,发现可以进一步优化状态。

我们发现,完全不需要记 \(f_{u, 2}\),所以我们状态 \(f_{u, 0/1}\) 变成了 \(u\) 是否有孩子的答案。

\[f_{u, 1} = 2^{\operatorname{siz}(u)}\sum _ {S \subseteq \operatorname{son}(u) \land S \neq \varnothing} \prod _ {v \in S} 2^{-1} g_v \]

这个简单容斥以下等价于下者:

\[f_{u, 1} = 2^{\operatorname{siz}(u)} \Big(\prod _ {v \in \operatorname{son}(u)} (2^{-1} g_v + 1) - 1\Big) \]

于是又可以解决问题了。我们进一步发现,在转移的时候,自始至终都在使用 \(g_v\),所以可以把 \(f_{u, 0/1}\) 合并起来。

代码

轻松最优解

#include <cstdio>
#include <iostream>
using namespace std;

const int mod = 1e9 + 7, inv2 = (mod + 1) >> 1;
inline int add(int a, int b) { return a >= mod - b ? a + b - mod : a + b; }
inline int sub(int a, int b) { return a < b ? a - b + mod : a - b; }
inline int mul(int a, int b) { return 1ll * a * b % mod; }

const int N = 500010, M = 1000010;

struct Graph {
    struct node {
        int to, nxt;
    } edge[M << 1];
    int head[N], tot = 1;
    inline void add(int u, int v) {
        edge[++tot] = { v, head[u] };
        head[u] = tot;
    }
    inline node & operator [] (int x) {
        return edge[x];
    }
} xym, yzh;

int n, m;

int dfn[N], low[N], timer;
int stack[N], top;
int sccno[N], scc_cnt, siz[N];

void tarjan(int u, int fr) {
    dfn[u] = low[u] = ++timer, stack[++top] = u;
    for (int i = xym.head[u]; i; i = xym[i].nxt) {
        if ((i ^ 1) == fr) continue;
        int v = xym[i].to;
        if (!dfn[v]) tarjan(v, i), low[u] = min(low[u], low[v]);
        else low[u] = min(low[u], dfn[v]);
    }
    if (low[u] >= dfn[u]) {
        ++scc_cnt;
        do {
            int v = stack[top--];
            sccno[v] = scc_cnt;
            ++siz[scc_cnt];
        } while (stack[top + 1] != u);
    }
}

int pw[M], f[N], ans;

void dfs(int u, int fa) {
    int one = 1;
    for (int i = yzh.head[u]; i; i = yzh[i].nxt) {
        int v = yzh[i].to;
        if (v == fa) continue;
        dfs(v, u);
        ans = sub(ans, mul(inv2, f[v]));
        one = mul(one, add(mul(inv2, f[v]), 1));
    }
    f[u] = mul(sub(one, 1), pw[siz[u]]);
    f[u] = add(f[u], sub(pw[siz[u]], 1));
    ans = add(ans, f[u]);
}

signed main() {
    #ifndef XuYueming
    freopen("barrack.in", "r", stdin);
    freopen("barrack.out", "w", stdout);
    #endif
    scanf("%d%d", &n, &m);
    for (int i = 1, u, v; i <= m; ++i) {
        scanf("%d%d", &u, &v);
        xym.add(u, v), xym.add(v, u);
    }
    tarjan(1, 0);
    for (int u = 1; u <= n; ++u)
        for (int i = xym.head[u]; i; i = xym[i].nxt) {
            int v = xym[i].to;
            if (sccno[u] == sccno[v]) continue;
            yzh.add(sccno[u], sccno[v]);
        }
    pw[0] = 1;
    for (int i = 1; i <= max(n, m); ++i) pw[i] = add(pw[i - 1], pw[i - 1]);
    dfs(1, 0), ans = mul(ans, pw[m]);
    printf("%d", ans);
    return 0;
}
posted @ 2024-11-03 20:12  XuYueming  阅读(31)  评论(0编辑  收藏  举报