「NOIP2022」建造军营 题解

前言

题目链接:洛谷

题意简述

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

1n5×105n1m106,保证图连通,无自环、重边。

题目分析

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

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

形式化地表示如下:

ans=S{i}i=1n2mcalc(S)uS(2siz(u)1)

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

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

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

fu,0 最简单,不需要转移,为 2siz(u)1

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

fu,1=2siz(u)21vson(u)gv

fu,2 直接来不好做,需要和 fu,1 一起算。我们顺次枚举孩子 v,先让 fu,2fu,2+(fu,1+fu,2)21gv,再 fu,1fu,1+2siz(u)21gv。前者表示讨论是否选择 (u,v) 再根据加法原理累加。

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

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

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

fu,1=2siz(u)Sson(u)SvS21gv

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

fu,1=2siz(u)(vson(u)(21gv+1)1)

于是又可以解决问题了。我们进一步发现,在转移的时候,自始至终都在使用 gv,所以可以把 fu,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 @   XuYueming  阅读(31)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 【杭电多校比赛记录】2025“钉耙编程”中国大学生算法设计春季联赛(1)
点击右上角即可分享
微信分享提示