前言
題目連結:洛谷。
題意簡述
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}\)。
形式化地表示如下:
這樣一坨東西應該需要樹形 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, 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\) 是否有孩子的答案。
這個簡單容斥以下等價於下者:
於是又可以解決問題了。我們進一步發現,在轉移的時候,自始至終都在使用 \(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;
}