【二次扫描与换根】总结
一、定义
在一棵无根树中,若需要分别计算以多个节点为根的多个答案,而当节点总数
二、解法
解决方法如下:换根 dp 通常会与树形 dp 结合,我们可以先任定一个根节点
在代码中,我们一共需要两个
三、例题
1、Sta
分析:
此题非常经典和模板。我们可以先求解出以节点 1 为根节点时的答案,再考虑换根。
见下图:
显然,当 1 的儿子节点 3 成为根时,3 和 3 的子树中的所有节点的深度都会减 1,而其它节点的深度都会加 1。推广一下即得:设以
Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
#define int long long
struct Edge {
int to, next;
}edge[2000005];
int head[1000005], cnt, dep[1000005], siz[1000005], dp[1000005], n;
void add(int u, int v) {
edge[++cnt].to = v;
edge[cnt].next = head[u];
head[u] = cnt;
}
void dfs1(int x, int fa) {
dep[x] = dep[fa] + 1;
siz[x] = 1;
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
dfs1(to, x);
siz[x] += siz[to];
}
}
void dfs2(int x, int fa) {
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
dp[to] = dp[x] - siz[to] + (n - siz[to]);
dfs2(to, x);
}
}
signed main() {
SF("%lld", &n);
for(int i = 1; i < n; i++) {
int u, v;
SF("%lld%lld", &u, &v);
add(u, v), add(v, u);
}
dfs1(1, 0);
for(int i = 1; i <= n; i++) dp[1] += dep[i];
dfs2(1, 0);
int ans = 0, index;
for(int i = 1; i <= n; i++) {
if(dp[i] > ans) ans = dp[i], index = i;
}
PF("%lld", index);
return 0;
}
2、积蓄程度
分析:
我们先考虑一个节点最多能蓄的水受哪些因素影响。
首先,若它的所有儿子最多只能蓄
现在思考如何换根。
令
方程式可写作:
但有一种特殊情况:如果
Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
#define int long long
struct Edge {
int to, next, w;
}edge[400005];
int head[200005], cnt, dp[2][200005], n, ans[200005], d[200005];
// 0 儿子最多能装的 1 自己最多能装的
void add(int u, int v, int w) {
edge[++cnt].to = v;
edge[cnt].next = head[u];
edge[cnt].w = w;
head[u] = cnt;
}
void dfs1(int x, int fa, int Max) {
bool flag = true;
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
flag = false;
dfs1(to, x, edge[i].w);
dp[0][x] += dp[1][to];
}
dp[1][x] = min(Max, dp[0][x]);
if(flag) dp[1][x] = Max;
}
void dfs2(int x, int fa) {
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
if(d[x] == 1) ans[to] = dp[0][to] + edge[i].w;
else ans[to] = dp[0][to] + min(ans[x] - dp[1][to], edge[i].w);
dfs2(to, x);
}
}
signed main() {
int t;
SF("%lld", &t);
while(t--) {
cnt = 0;
memset(head, 0, sizeof(head));
memset(dp, 0, sizeof(dp));
SF("%lld", &n);
for(int i = 1; i < n; i++) {
int u, v, w;
SF("%lld%lld%lld", &u, &v, &w);
d[u]++, d[v]++;
add(u, v, w), add(v, u, w);
}
dfs1(1, 0, 1e18);
ans[1] = dp[1][1];
dfs2(1, 0);
int sum = 0;
for(int i = 1; i <= n; i++) sum = max(sum, ans[i]);
PF("%lld\n", sum);
}
return 0;
}
3、概率充电器
分析:
首先我们要知道期望是什么。
现有
在此题中,我们对每个元件进入充电状态的期望固定为 1 ,因此我们只需要计算所有元件进入充电状态的概率的总和就行了。
其次,我们要了解最基本的有关于概率的知识。
假设现有两个事件
结论:概率为
将它们相加即可。
现在回到题目里。一个元件的电有三个来源:来自父亲,来自自己,来自儿子。后两个较好解决。设第
具体即为:
现在考虑第一种情况:来自父亲。
若父亲
根据换根 dp 的性质,我们已经得到了
得到这些之后,转移出
所以套用公式,即可得到:
Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
struct Edge {
int to, next;
double w;
}edge[1000005];
int head[500005], cnt;
double dp[500005];
const double eps = 1e-8;
void add(int u, int v, double w) {
edge[++cnt].to = v;
edge[cnt].next = head[u];
edge[cnt].w = w;
head[u] = cnt;
}
void dfs1(int x, int fa) {
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
dfs1(to, x);
dp[x] = dp[x] + dp[to] * edge[i].w - dp[x] * dp[to] * edge[i].w;
}
}
void dfs2(int x, int fa) {
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
if(fabs(dp[to] * edge[i].w - (double)(1.0)) <= eps) dfs2(to, x); //判分母是否为0
else {
double a = (dp[x] - dp[to] * edge[i].w) / (1 - dp[to] * edge[i].w);
dp[to] = dp[to] + a * edge[i].w - dp[to] * a * edge[i].w;
dfs2(to, x);
}
}
}
int main() {
int n;
SF("%d", &n);
for(int i = 1; i < n; i++) {
int u, v;
double w;
SF("%d%d%lf", &u, &v, &w);
add(u, v, w / 100.0), add(v, u, w / 100.0);
}
for(int i = 1; i <= n; i++) SF("%lf", &dp[i]), dp[i] /= 100.0;
dfs1(1, -1);
dfs2(1, -1);
double ans = 0;
for(int i = 1; i <= n; i++) ans += dp[i];
PF("%.6lf", ans);
return 0;
}
4、计算机
分析:
设节点
在换根 dfs 中考虑第二种情况。
若父亲
什么,你想 Hack 我?
确实很好 Hack ,请看下图:
令此图中所有的边权都为 1 ,显然
我们设节点
当然,在进行换根 dp 的同时需要实时更新
Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
struct Edge {
int to, next, w;
}edge[20005];
int head[10005], cnt, Max[2][10005], dp[10005], p[10005];
// 0 最大 1 次大
void add(int u, int v, int w) {
edge[++cnt].to = v;
edge[cnt].next = head[u];
edge[cnt].w = w;
head[u] = cnt;
}
void dfs1(int x, int fa) {
Max[0][x] = 0, Max[1][x] = 0x3f3f3f3f;
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
dfs1(to, x);
int now = Max[0][to] + edge[i].w;
if(now > Max[0][x]) Max[1][x] = Max[0][x], Max[0][x] = now, p[x] = to;
else if(now > Max[1][x]) Max[1][x] = now;
}
}
void dfs2(int x, int fa) {
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
if(to == p[x]) {
Max[0][to] = max(dp[x] - edge[i].w, edge[i].w + Max[1][x]);
Max[1][to] = max(Max[1][to], min(dp[x] - edge[i].w, edge[i].w + Max[1][x]));
if(edge[i].w + Max[1][x] == Max[0][to]) p[to] = x;
else p[to] = p[p[x]];
dp[to] = Max[0][to];
}
else {
Max[0][to] = dp[to] = dp[x] + edge[i].w;
p[to] = x;
}
dfs2(to, x);
}
}
int main() {
int n;
while(SF("%d", &n) != EOF) {
memset(Max, 0, sizeof(Max));
memset(dp, 0, sizeof(dp));
memset(head, 0, sizeof(head));
cnt = 0;
for(int i = 2; i <= n; i++) {
int u, w;
SF("%d%d", &u, &w);
add(i, u, w), add(u, i, w);
}
dfs1(1, -1);
dp[1] = Max[0][1];
dfs2(1, -1);
for(int i = 1; i <= n; i++) PF("%d\n", dp[i]);
}
return 0;
}
5、连珠线
本人觉得最有意思同时也是最难的一道题目
提醒:该题目会用到上一题的思想,建议弄懂上一题后再进行阅读。
拿到题之后很可能会看不懂。没关系,我们要学会转化题意。
给出一棵树,一些边是蓝色的,剩下的边都是红色的。蓝色的边权值总和作为答案,求答案可能的最大值。
当然蓝色的边是有限制条件的。不然还需要你来做?,见下图,当树的结构形如。
或者
时,其边可以为蓝色。
其实我们把第一棵树拉直后,结构也就变成了第二棵树。
问题来了,怎么求呢?
我们设
第一种情况稍微好处理一点。设当前节点
接下来考虑第二种情况:
如果
但是好像有什么问题?
显然,选出来的最优节点
因此最终的转移方程式可记作:
然后考虑如何换根。
为了避免混淆之前得到的答案,我们令
设当前节点
那么
类比上面的做法,我们在第一次 dfs 的时候用一个数组
但是这真的正确吗?
和上一题的思想一样,如果
所以也需要一个
当然,这并不是最终的
那么为什么不用考虑
因为我们在计算的时候已经得到了最终答案
虽然但是,在计算
当然不行!
还得把父亲
由于根节点一定是作为蓝线的中心节点的,所以可写作:
至此,本题完美结束。可以看一下代码,也许会思路更清晰一点。
Code:
#include<bits/stdc++.h>
using namespace std;
#define SF scanf
#define PF printf
struct Edge {
int to, next, w;
}edge[400005];
int head[200005], cnt, dp[2][200005], now[2][2000005], Max[2][200005], sum[200005], p[200005];
// 0 不是中点 1 是中点
// 0 最大 1 次大
void add(int u, int v, int w) {
edge[++cnt].to = v;
edge[cnt].next = head[u];
edge[cnt].w = w;
head[u] = cnt;
}
void dfs1(int x, int fa) {
Max[0][x] = Max[1][x] = -0x3f3f3f3f;
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
dfs1(to, x);
dp[0][x] += max(dp[1][to] + edge[i].w, dp[0][to]);
int now = dp[0][to] + edge[i].w - max(dp[1][to] + edge[i].w, dp[0][to]);
if(now > Max[0][x]) Max[1][x] = Max[0][x], Max[0][x] = now, p[x] = to;
else if(now > Max[1][x]) Max[1][x] = now;
}
dp[1][x] = dp[0][x] + Max[0][x];
}
void dfs2(int x, int fa, int w) {
for(int i = head[x]; i; i = edge[i].next) {
int to = edge[i].to;
if(to == fa) continue;
int k = Max[0][x];
if(p[x] == to) k = Max[1][x];
now[0][x] = sum[x] - max(dp[1][to] + edge[i].w, dp[0][to]);
now[1][x] = now[0][x] + k;
if(fa != -1) now[1][x] = max(now[1][x], now[0][x] + now[0][fa] + w - max(now[1][fa] + w, now[0][fa]));
sum[to] = dp[0][to] + max(now[0][x], now[1][x] + edge[i].w);
dfs2(to, x, edge[i].w);
}
}
int main() {
int n, ans = 0;
SF("%d", &n);
for(int i = 1; i < n; i++) {
int u, v, w;
SF("%d%d%d", &u, &v, &w);
add(u, v, w), add(v, u, w);
}
dfs1(1, -1);
sum[1] = dp[0][1];
dfs2(1, -1, 114514);
for(int i = 1; i <= n; i++) ans = max(ans, sum[i]);
PF("%d", ans);
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 使用C#创建一个MCP客户端
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列1:轻松3步本地部署deepseek,普通电脑可用
· 按钮权限的设计及实现