Codeforces 换根 DP 三题:距离和、树上涂色与最大白子树
普通树形 DP 常问“根固定为 1 时,某个子树的答案是什么”;换根 DP 再追问一步:如果把根从父节点移到相邻孩子,哪些贡献改变了?在树上删掉这条边后,全部顶点只分成孩子侧与其余侧,所以重新计算通常可以压缩成一次加、一次减。
本文用三题建立一条递进路线:先处理可以写成总和的距离代价,再识别 Tree Painting 隐藏的深度和,最后处理不能用单个全局总量概括、需要传递“父侧最优贡献”的最大连通子图。
1. 官方资料与学习目标
| 题目 | 官方评分 | 官方标签 | 约束 | 本文核心 |
|---|---|---|---|---|
| 1092F Tree with Maximum Cost | 1900 | dfs and similar、dp、trees | n≤2×10^5,a_i≤2×10^5 |
加权距离和的换根公式 |
| 1187E Tree Painting | 2100 | dfs and similar、dp、trees | n≤2×10^5 |
把游戏得分改写成深度和 |
| 1324F Maximum White Subtree | 1800 | dfs and similar、dp、graphs、trees | n≤2×10^5 |
从子树最优扩展到父侧最优 |
三题都能在线性时间内完成。真正需要掌握的不是“写两遍 DFS”,而是下面这条推导链:
flowchart LR
A["先任选根 1"] --> B["自底向上统计子树信息"]
B --> C["分析父子边被切开后的两侧"]
C --> D["从父答案 O(1) 推出孩子答案"]
D --> E["沿树传播到所有根"]
代码全部使用父节点数组与遍历序列,避免在 n=2×10^5 的链上递归过深。
2. 换根为什么只需要看一条边
先以 1 为根,考虑父节点 u 与孩子 v。删掉边 u-v 后:
v的原子树在一侧;- 剩余所有点在另一侧;
- 从根
u移到v时,第一侧的距离全部减 1,第二侧的距离全部加 1。
flowchart LR
L["v 的原子树:距离全部减 1"] --- E["边 u-v"]
E --- R["其余部分:距离全部加 1"]
因此,只要自底向上预先算出 v 这一侧的总量,就不必为每个新根重新遍历整棵树。
3. 第一题:1092F Tree with Maximum Cost
3.1 目标与朴素方法
每个点 i 有正权 a_i。选择根 r 后,代价为:
$$F(r)=\sum_i a_i\cdot dist(i,r).$$
需要输出最大的 F(r)。如果对每个根分别 BFS/DFS,复杂度是 O(n^2),链形树在最大规模下无法运行。
3.2 固定根时要保存什么
先以 1 为根,定义:
sub[v]:v原子树内的权值总和;total:全树权值总和;F(1):遍历时把depth[v] * a[v]累加。
自底向上有:
$$sub[u]=a_u+\sum_{v\text{ 是 }u\text{ 的孩子}}sub[v].$$
3.3 换根公式
根从 u 移到孩子 v:
v子树内权值共sub[v],距离各减 1,贡献减少sub[v];- 子树外权值共
total-sub[v],距离各加 1,贡献增加total-sub[v]。
所以:
$$F(v)=F(u)-sub[v]+(total-sub[v])=F(u)+total-2sub[v].$$
| 部分 | 权值和 | 距离变化 | 对答案的改变量 |
|---|---|---|---|
v 子树 |
sub[v] |
-1 |
-sub[v] |
| 其余顶点 | total-sub[v] |
+1 |
+total-sub[v] |
3.4 手工演算
链 1-2-3-4 的权值为 [2,1,3,2],以 1 为根:
| 根 | 加权距离和 |
|---|---|
| 1 | 0×2+1×1+2×3+3×2=13 |
| 2 | 13+8-2×6=9 |
| 3 | 9+8-2×5=7 |
| 4 | 7+8-2×2=11 |
最大值是 13。这里 sub[2]=6、sub[3]=5、sub[4]=2,每一步都只用上一个根的答案。
3.5 C++17 实现
1 |
|
时间 O(n),空间 O(n)。最大答案可能达到约 8×10^15,必须使用 long long。
4. 第二题:1187E Tree Painting
4.1 先把游戏翻译成静态量
开始时全白。第一次任选一个点涂黑,之后每次只能选与黑点相邻的白点;得分是被选点所在白色连通块的大小。
选定第一个点 r 后,把树看成以 r 为根:一个点只有在父节点已经黑色后才能被选到,而它的后代此时仍是白色。因此选点 v 时,白色连通块恰好是 v 的子树,得分为 size[v]。
总分为:
$$S(r)=\sum_v size_r[v].$$
每个点 x 会被自己及所有祖先的子树各统计一次,共 depth_r[x]+1 次,所以又有:
$$S(r)=\sum_x(depth_r[x]+1)=n+\sum_x depth_r[x].$$
原来的操作顺序消失了,问题变成“选择一个根,使所有点深度和最大”。
flowchart TD
A["选定第一个黑点 r"] --> B["合法顺序必然先父后子"]
B --> C["选择 v 时得分等于其子树大小"]
C --> D["总分等于所有子树大小之和"]
D --> E["等于 n 加所有点深度之和"]
4.2 换根公式
这次每个点权值都是 1。若初始根为 1,先算 size[v] 与:
$$S(1)=n+\sum_v depth[v].$$
根从 u 移到孩子 v 时,v 子树的 size[v] 个点深度减 1,其余 n-size[v] 个点深度加 1:
$$S(v)=S(u)+n-2size[v].$$
这正是上一题公式令所有 a_i=1 后的形式,只多了对所有根相同的常数 n。
4.3 手工演算
树边为 1-2、1-3、2-4、2-5,以 1 为根:
| 项目 | 值 |
|---|---|
| 深度 | [0,1,1,2,2] |
S(1) |
5+(0+1+1+2+2)=11 |
size[2] |
3 |
S(2) |
11+5-2×3=10 |
size[3] |
1 |
S(3) |
11+5-2×1=14 |
点 3 看似是叶子,却能让大多数点离根更远,因此取得更高总分。
4.4 C++17 实现
1 |
|
时间 O(n),空间 O(n)。
5. 第三题:1324F Maximum White Subtree
5.1 把颜色变成权值
白点记为 +1,黑点记为 -1。对每个点 v,要求一个包含 v 的连通子图,使点权和最大。
固定根 1,先定义:
$$down[u]=w_u+\sum_{v\text{ 是孩子}}\max(0,down[v]).$$
如果某个孩子方向的最优贡献为负,就不接入;为正才值得保留。down[u] 只看得到原子树,还不是最终答案。
5.2 父侧怎样传给孩子
假设已经知道 answer[u],要转移到孩子 v:
answer[u]中可能包含v方向的正贡献,先减掉max(0,down[v]);- 得到从
u通往树外其余部分的贡献outside; - 对
v来说,只有outside>0才值得接入。
于是:
$$outside=answer[u]-\max(0,down[v]),$$
$$answer[v]=down[v]+\max(0,outside).$$
flowchart LR
A["answer[u]:包含 u 的全树最优"] --> B["减去 v 方向已计入的正贡献"]
B --> C["得到父侧 outside"]
C --> D{"outside 是否为正"}
D -->|是| E["接到 down[v] 上"]
D -->|否| F["只保留 down[v]"]
5.3 手工演算
链 1-2-3-4 颜色为黑、白、白、黑,对应 [-1,+1,+1,-1],以 1 为根:
| 点 | down |
解释 |
|---|---|---|
| 4 | -1 | 只有黑点自己 |
| 3 | 1 | 不接负贡献的点 4 |
| 2 | 2 | 接入点 3 |
| 1 | 1 | 自己 -1,加上点 2 方向的 2 |
向下换根后,点 3 可以从父侧接入点 2 的正贡献,所以 answer[3]=2;点 4 也可接入点 2、3,得到 1。最终答案 [1,2,2,1]。
5.4 C++17 实现
1 |
|
时间 O(n),空间 O(n)。
6. 三题的换根状态有什么区别
| 题目 | 第一遍统计 | 第二遍传递 | 能否只靠一个全局总量 |
|---|---|---|---|
| 1092F | 子树权值和、根 1 的加权距离和 | +total-2×sub[v] |
可以 |
| 1187E | 子树大小、根 1 的深度和 | +n-2×size[v] |
可以 |
| 1324F | 子树内包含当前点的最大贡献 | 去掉孩子贡献后传父侧正贡献 | 不可以 |
前两题的目标对所有点贡献做线性求和,切边后只需知道两侧的“量”;第三题带有“负贡献可以不选”的最优化决策,必须保留方向性状态。
7. 正确性证明
7.1 1092F
固定边 u-v 且 u 是 v 的父亲。换根到 v 后,v 原子树内每个点距离恰减 1,其余点距离恰加 1。按权值求和得到 F(v)=F(u)+total-2sub[v]。根 1 的值直接计算正确;沿父子边传播覆盖所有点,故每个根的值正确,取最大即为答案。
7.2 1187E
选定首个黑点后,树的唯一简单路径保证任何点被选前其父亲必须已黑,而其未选后代仍构成白色连通块,所以该步得分等于当前根下的子树大小。所有子树大小之和等于每点的祖先数之和,即 n+深度和。换根的深度变化与 1092F 全部点权为 1 时相同,因此公式与最大值均正确。
7.3 1324F
对固定根,包含 u 且限于其子树的最优连通块,在每个孩子方向可以独立决定接入或舍弃;接入的最优收益为 down[v],只有为正时有利,因此 down 转移正确。计算孩子 v 时,从 answer[u] 移除其中可能采用的 v 方向,剩余就是父侧可提供的最优连通贡献;同样只在为正时接入。树上各方向被恰好考虑一次,故 answer[v] 是包含 v 的全树最优值。
8. 边界与错误清单
- 把换根写成每个根重新 DFS。 复杂度会退化到
O(n^2)。 - 1092F 用
int。 权值、距离与求和相乘后必须用long long。 - 换根时只减子树,不加外部。 一条边的两侧距离变化方向相反。
- Tree Painting 直接搜索涂色顺序。 顺序不是状态;固定首点后总分已经由根树确定。
- 误写
S(r)=深度和。 每个点还会被自己的子树统计一次,需要加n。 - 1324F 强行接入负贡献。 每个方向都应取
max(0, contribution)。 - 计算父侧时不移除孩子贡献。 会把
v方向重复计入。 - 递归 DFS 直接跑 20 万点长链。 C++ 默认栈可能溢出,迭代遍历更稳妥。
- 父节点数组只判断
next != parent,却没有建立树。 应保证每个点只从父亲首次进入;树输入下本文写法成立。 - 忘记单点与全黑情况。 1092F 单点答案为 0;1324F 全黑时每点答案为 -1。
9. 独立验证
tests/verify-rerooting-three-article.cjs 会提取并无警告编译三份 C++17 代码,并采用不同思路核验:
- 1092F 对小树枚举每个根,用 BFS 直接求所有距离与加权和;
- 1187E 对每个候选首点直接计算
n+深度和; - 1324F 枚举所有点集,检查连通性,并为每个必含点寻找最大权值和;
- 三题另用 20 万点长链检查迭代遍历、64 位上界与线性复杂度。
10. 下一步
可以先回看树上 DFS 三题,确认深度、路径和子树贡献的基本语义;再读本文理解根移动时怎样复用答案。之后可进入树的直径、最近公共祖先或树链剖分:它们同样利用唯一简单路径,但维护的信息不再只沿一条父子边更新。