「NOIP2022」建造軍營 題解

XuYueming發表於2024-11-03

前言

題目連結:洛谷

題意簡述

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;
}

相關文章