[NOIP2022] 建造军营 题解
[NOIP2022] 建造军营 题解
Part I 观察
注意到如果删掉的边在一个边双连通分量里面,那么无论如何都不会影响 A 国,所以 B 国只会删掉桥,于是把图边双缩点之后,同一个边双里面的点要么都不选,要么随便选至少一个。
Part II DP
再次发现军营一定是一个极大的连通块,所以可以考虑设计状态:
\(f_{i, 0}\) 表示以 \(i\) 为根的子树里面没有军营的方案数,\(f_{i, 1}\) 表示以 \(i\) 为根的子树里面有军营的方案数,且 \(i\) 在这个军营连通块之中的方案数。
于是对于 \((u, v)\in E\),可以通过讨论 \(u, v\) 是否选择军营转移,此处 \(f_{u, x}\) 的状态是滚动的,也就是说其保存的实际上是 \(v\) 之前的儿子的方案数:
\[f_{u, 0} = f_{u, 0}\times f_{v, 0}\times 2
\]
如果 \(u\) 子树里面没有军营,其他儿子肯定也没有,并且 \(v\) 也不能有。
如果 \(u\) 子树里面有军营,那么分三种讨论:
- \(u\) 其他儿子有军营,\(v\) 有军营
- \(u\) 其他儿子有军营,\(v\) 没有军营
- \(u\) 其他儿子没有军营,\(v\) 有军营
三种情况互相独立,所以加起来,\(u, v\) 是否有军营是有依赖的,所以乘起来。
\[f_{u, 1} = f_{u, 1}\times f_{v, 1} + f_{u, 1}\times f_{v, 0}\times 2+f_{u, 0}\times f_{v, 1}
\]
第二种情况下,\((u, v)\) 这条边选不选是无关紧要的,所以要 \(\times 2\),其他情况下,为了让 \(u\) 和军营连通块相连,\((u, v)\) 是必选的。
Part III 统计答案
可以枚举军营连通块的根来统计答案,如果军营的根 \(u\) 不在根节点上,那么它对答案的贡献是:
\[f_{u, 1}\times 2^{m - cnt_u - 1}
\]
也就是所有的边是否选取,其中 \((u, fa_u)\) 必不选,因为如果选择了,军营连通块的根就不在 \(u\) 上了,所以指数要减去 \(1\)。
另外所有在 \(u\) 子树内部的边(桥)也已经确定了,所以指数要减去 \(cnt_u\)。
而对于根节点,它没有父亲,所以它的贡献是:
\[f_{root, 1} \times 2^{m - cnt_{root}}
\]
Part IV C++ 实现
// Problem: P8867 [NOIP2022] 建造军营
// Contest: Luogu
// Author: Moyou
// Copyright (c) 2023 Moyou All rights reserved.
// Date: 2023-11-15 19:53:39
#include <algorithm>
#include <cstring>
#include <iostream>
#include <queue>
// #define int long long
#define x first
#define y second
using namespace std;
typedef pair<int, int> PII;
typedef long long ll;
const int N = 5e5 + 10, M = 1e6 + 10, mod = 1e9 + 7;
int n, m;
vector<int> g[N], G[N];
int dfn[N], low[N], timestamp, stk[N], top, id[N], sz[N], scc;
void tarjan(int u, int fa) {
dfn[u] = low[u] = ++ timestamp, stk[++ top] = u;
for(auto v : g[u]) {
if(v == fa) continue;
if(!dfn[v]) tarjan(v, u), low[u] = min(low[u], low[v]);
else low[u] = min(low[u], dfn[v]);
}
if(low[u] ^ dfn[u]) return ;
int y; scc ++;
while(y != u) {
y = stk[top --];
id[y] = scc, sz[scc] ++;
}
}
int f[N][2]; // f[i][1] refers that there are barracks in subtree i
// f[i][0] refers that there is no brck in subtree i
int p[M], cnt[N];
void dfs(int u, int fa) {
f[u][0] = 1, f[u][1] = p[sz[u]] - 1; // notice that there must be one brks in u
for(auto v : G[u]) {
if(v == fa) continue;
dfs(v, u);
cnt[u] += cnt[v] + 1;
int t0 = f[u][0], t1 = f[u][1];
f[u][0] = 2ll * f[v][0] * f[u][0] % mod;
f[u][1] = ((1ll * f[v][1] * t0 % mod + 2ll * f[v][0] * t1 % mod) % mod + 1ll * f[v][1] * t1 % mod) % mod;
}
}
signed main() {
ios::sync_with_stdio(0), cin.tie(0), cout.tie(0);
cin >> n >> m;
for(int i = 1, a, b; i <= m; i ++) {
cin >> a >> b;
g[a].push_back(b), g[b].push_back(a);
}
p[0] = 1;
for(int i = 1; i <= m; i ++) p[i] = p[i - 1] * 2 % mod;
for(int i = 1; i <= n; i ++) if(!dfn[i]) tarjan(i, 0);
for(int i = 1; i <= n; i ++)
for(auto v : g[i])
if(id[i] != id[v])
G[id[i]].push_back(id[v]);
dfs(1, 0);
int ans = 0;
for(int i = 2; i <= scc; i ++)
ans = (ans + 1ll * f[i][1] * p[m - cnt[i] - 1] % mod) % mod;
cout << ((ans + 1ll * f[1][1] * p[m - cnt[1]] % mod) % mod + mod) % mod << '\n';
return 0;
}