【二次扫描与换根】总结
前言#
前置知识:树形DP。
引入&思想#
换根 DP 通常与树形 DP 相联系,在进行树形 DP 时,根节点通常是确定的,解决方案大多是从根节点开始递归。但一类题目不会指定根结点,且根结点的变化会对答案产生影响,暴力枚举每一个节点为根节点又会在时间复杂度上出问题,这时可以考虑使用换根 DP 解决。
换根 DP 的思路是使用两次 DFS,第一次确定以某一节点为根节点时的答案,第二次以刚才确定的根节点出发,在每次递归前都自顶向下进行状态转移,用父节点的状态更新子节点的状态,计算出“换根”后的结果。
接下来以例题为背景分析换根 DP 的思路。
例题#
[POI2008] STA-Station#
题意清晰明了,不再重复。
考虑转换根节点会发生什么。


如图,当树的根节点从5变成4后,原以4为根的子树的节点深度全部减一,除开这些节点其他节点深度全部加一。由此推导得出状态转移方程:
分析第二个 DFS ,也就是换根的实现过程:
void Tr_DP(ll u, ll fa) {
for (int i = 0; i < Gra[u].size(); i ++){
ll v = Gra[u][i];
if (v == fa){
continue;
}
dp[v] = dp[u] - si[v] + (n - si[v]);//根据父节点u求出以v为根时的答案
Tr_DP(v, u);//已得到以v为根时的答案,根据此继续解决v的子节点
}
}
[APIO2014] 连珠线#
题目大意: 开始只有一个结点。两种操作:可以用一条红线将一个新点连向旧点。或者是选择一条红线,用一个新点和两条蓝线连接原先红线连接的两个点。现在给出最终的连线情况,问蓝线可能的最大长度是多少。
分析:因为始终出现的新节点会只连上一个旧节点,或在两个已经有关系的旧点中间出现,可以分析得出最后的图为一棵树(严谨看待,如果题目没有说明,应该会出现将树认成图的情况)。
所以蓝线的连线情况只可能为两种:

但如图,如果为后者,也就是
考虑设计状态:
不难发现,
对于
对于
记录
根据状态转移方程可以轻松得出以某一结点为根时的答案,接下来就考虑换根。
考虑两种情况:
情况一:换的子节点不为根节点的
情况二:换的子节点是根节点的
两次 DFS 代码:
void Tr_dp(ll u,ll fa) {
mx[u] = mn[u] = -0x3f3f3f3f3f3f;
for (int i = 0; i < Gra[u].size(); i ++){
ll v = Gra[u][i].v, w = Gra[u][i].w;
if (v == fa){
continue;
}
Tr_dp(v, u);
ll k = max(dp[v][0], dp[v][1] + w);
dp[u][0] += k;
if (dp[v][0] + w - k > mx[u]){
mn[u] = mx[u];
mx[u] = dp[v][0] + w - k;
p[u] = v;
}
else if (dp[v][0] + w - k > mn[u]){
mn[u] = dp[v][0] + w - k;
}
}
dp[u][1] = dp[u][0] + mx[u];
}
void Tr_DP(int u, int fa, int val){
int k = max(dp2[fa][0], dp2[fa][1] + val);
dp[u][0] += k;
if (fa && dp2[fa][0] + val - k > mx[u]){
mn[u] = mx[u];
mx[u] = dp2[fa][0] + val - k;
p[u] = fa;
}
else if (fa && dp2[fa][0] + val - k > mn[u]){
mn[u] = dp2[fa][0] + val - k;
}
dp[u][1] = dp[u][0] + mx[u];
ans = max(ans, dp[u][0]);
for (int i = 0; i < Gra[u].size(); i ++){
ll v = Gra[u][i].v, w = Gra[u][i].w;
if (v == fa){
continue;
}
dp2[u][0] = dp[u][0] - max(dp[v][0], dp[v][1] + w);
if (v == p[u]){
dp2[u][1] = dp2[u][0] + mn[u];
}
else {
dp2[u][1] = dp2[u][0] + mx[u];
}
Tr_DP(v, u, w);
}
}
[SHOI2014] 概率充电器#
前置知识:概率
大概题意:给你一棵树,树上每一点有直接通电的概率,每条边有导电的概率。求出期望有电节点数。
分析:
不难发现能使一个节点
节点
儿子节点给节点
父亲节点给节点
难点来了:如何状态转移使 DP 没有后效性?
因为我们发现根节点不受父亲的影响,考虑先忽略父亲节点给节点
设
将
其中
所以我们能够发现除开节点
然后考虑
设
所以父节点除开
所以
所以
所以
最终的答案即使所有节点有电的概率和。
两次 DFS 代码:
void Tr_dp(ll u, ll fa) {
dp[u] = 1.0 - co[u] * 1.0 / 100;
for (int i = 0; i < Gra[u].size(); i ++){
ll v = Gra[u][i].v;
db w = Gra[u][i].w * 1.0 / 100;
if (v == fa){
continue;
}
Tr_dp(v, u);
dp[u] *= (dp[v] + (1 - dp[v]) * (1 - w));
}
}
void Tr_DP(ll u, ll fa) {
db te;
for (int i = 0; i < Gra[u].size(); i ++){
ll v = Gra[u][i].v;
db w = Gra[u][i].w * 1.0 / 100;
if (v == fa){
db t;
if (dp[u] + (1 - dp[u]) * (1 - w) == 0){
t = dp2[fa] * dp[fa];
}
else {
t = dp2[fa] * dp[fa] / (dp[u] + (1 - dp[u]) * (1 - w));
}
dp2[u] = (t + (1 - t) * (1 - w));
break;
}
}
if (fa == 0){
dp2[u] = 1;
}
for (int i = 0; i < Gra[u].size(); i ++){
ll v = Gra[u][i].v;
if (v == fa){
continue;
}
Tr_DP(v, u);
}
}
总结#
二次扫描与换根的题目算 DP,需要大量的思维难度,也需要笔和草稿纸的配合。
努力ing
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 单元测试从入门到精通
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律