[NOIP2022] 建造军营 题解

发布时间 2023-11-16 11:29:01作者: MoyouSayuki

[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\) 子树里面有军营,那么分三种讨论:

  1. \(u\) 其他儿子有军营,\(v\) 有军营
  2. \(u\) 其他儿子有军营,\(v\) 没有军营
  3. \(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;
}