前言
观前提醒:本文非新手向文章,不建议作为换根 DP 入门使用。
本文在洛谷专栏、博客园、CSDN同步发送。
换根 DP 是树状 DP 的一种,思维难度较高,但是学会以后很套路也很轻松。
例题
P3047 [USACO12FEB] Nearby Cows G
对于每个节点求出距离它不超过 \(k\) 的所有节点权值和 \(m_i\)。
\(\longrightarrow\) 这是换根 DP 的经典标志。
数据范围:\(n \le 10^5, k \le 20\)。
\(\longrightarrow\) 算法时空复杂度很可能是 \(O(nk)\)。
对以 \(1\) 为根的答案设计状态:由上文,首先考虑设计一个 \(n \times k\) 的状态,根据套路设计 \(f(x,d)\) 表示与 \(x\) 距离不超过 \(d\) 的节点权值和。
第一次 DFS 的转移方程:\(f(x,d)=\sum_{x \rightarrow y} f(y,d-1)\)。
然后 \(ans(1,i)=f(1,i)\)。
考虑第二次 DFS,利用“父子贡献传递”的思想,考虑父节点 \(x\) 变成子节点 \(y\) 的子节点时会发生什么。
首先 \(f(y,k)\) 表示 \(y\) 的子树的答案,但实际上应该加上把 \(x\) 也看成子树的贡献。
而把 \(x\) 看成 \(y\) 子树的贡献相当于 \(x\) 的原树 \(ans(x,k-1)\) 失去了以 \(y\) 为根的子树 $f(y,k-2) $后的 \(f'(x,k-1)\)。
此时这个 \(f'(x,k-1)=ans(x,k-1)-f(y,k-2)\),所以 \(ans(y,i)=f(y,i)+ans(x,i-1)-f(y,i-2)\)。
答案即为 \(ans(i,k)\)。
问:能否只用一个数组 \(f\),而不用开 \(ans\)?
答:可以。
在这样的换根 DP 中,以一个节点为根的答案在进入到这个节点之前就已经确定,是不会变的。所考虑的只有 \(f(y,i)\) 在转移时的新旧互相影响过程;
在上面的方程中,\(ans(y,i)=f(y,i)+ans(x,i-1)-f(y,i-2)\) 可以看到,新值=原来值+无所谓反正不变的值-旧值。每次用更新新值的必须是未被更新过的旧值,模仿 \(01\) 背包压维的过程,倒序遍历更新即可完美解决这个问题。
原代码第二次转移:
ans[y][0]=c[y],ans[y][1]=f[y][1]+c[x];
for(int j=2;j<=k;j++)ans[y][j]=f[y][j]+ans[x][j-1]-f[y][j-2];
新代码第二次转移:
for(int j=k;j>=2;j--)f[y][j]+=f[x][j-1]-f[y][j-2];
f[y][0]=c[y],f[y][1]+=c[x];
AT_dp_v Subtree
若只题目,逆元不模质数。
“对于每一个节点” \(\longrightarrow\) 尝试换根 DP。
数据范围:\(n \le 10^5\) \(\longrightarrow\) 时空复杂度 \(O(n)\)(因为换根 DP 一般都是 \(O(n)\))。
对第一次 DFS 设计状态:套路设计,\(f_i\) 表示以 \(i\) 为根的子树的答案(\(i\) 强制染黑)。
转移方程:某个点 \(x\) 染成黑色以后,子树要么不染(\(1\) 中方案),要么染黑的时相连的(\(f_y\) 种方案),所以 \(f_x = \prod_{x \rightarrow y} (f_y+1)\)。
第二次 DFS:利用“父子贡献传递”的思想,考虑父节点 \(x\) 变成子节点 \(y\) 的子节点时会发生什么。
\(f_y\) 应该乘上 \(f^*_x+1\),而 \(f^*_x\) 表示 \(x\) 没有以 \(y\) 为根的子树时的答案,为 \(\frac{f'_x}{f_y+1}\),即 \(f'_y = f_y \times (\frac{f'_x}{f_y+1}+1)\)。
但是,这道题的模数 \(m\) 不!是!质!数!,所以没法逆元。
考虑 \(f'_x\) 的来源:\(f'_x=\) \(x\) 原父节点的贡献 \(\times\) \(x\) 原子节点的贡献,所以 \(f^*_x=\) \(x\) 原父节点的贡献 \(\times\) \(x\) 除了 \(y\) 的原子节点的贡献。
又因为 \(x\) 除了 \(y\) 的原子节点的贡献 \(=\) \(x\) 在 \(y\) 左侧子节点的贡献 \(\times\) \(x\) 在 \(y\) 右侧子节点的贡献,所以可以分别记录 \(x\) 子节点贡献的前后缀和 \(fl\) 和 \(fr\),因为最后还要乘上 \(x\) 原父节点的贡献,所以这也要单独记录为 \(up_x\)(此时可以发现 \(up_y = f^*_x+1\))。
最后新的方程为 \(up_y = fl_{pre(y)} \times fr_{next(y)} \times up_x +1\),\(f'_y = f_y \times up_y\)。
可以发现这道题中也是 \(y\) 的所有状态都在进入 \(y\) 之前确定,且 \(y\) 的新状态只与 \(y\) 的原状态和 \(x\) (不会变的)原状态有关,所以 \(f'\) 和 \(f\) 可以压成一个数组表示。
P6419 [COCI 2014/2015 #1] Kamp
独立切的紫,爽!
题目要求“对点 \(i = 1 \sim n\)” 均求出答案,所以考虑换根 DP。
I. 简化树
首先,要遍历树上一些点,可以联想到一个结论:“遍历树上所有点(实际上是所有叶节点),最后回到原位所需的最小总路程为树边权和的两倍”,但是这个结论中的情况仍和本题有所不同。
- 本题中不需要遍历所有点,而只需要遍历有标记的点即可。针对这个情况,需要找到那些不必遍历的点——这些点不在任何两个标记点的路径上。
- 本题中不需要回到原位,所以答案应为:两倍边权和减最后一个点的深度。
对于第一种情况,这类非必要点分布在两个位置:(1)没有标记点的子树中,这时可以直接砍断这个子树;(2)从根节点(含)到所有标记点的 LCA(不含)的所有点。
对于上述(2)情况,只要一开始把根设置成必要点上,那么(2)点就不存在,缩减了讨论范围。所以可以尝试从第一个标记点的位置开始遍历,以它为根。
这样我们就获得了一棵简化树,遍历这棵树上的所有节点也就相当于是完成了题目中的任务。
II. 必要点
因为答案为“两倍边权和减最后一个点的深度”,两倍边权和是定值,所以要使得最后一个点的深度最大,答案才能最小。
于是自然会选择让最深的一个点作为最后一个点(废话),考虑如何找到以每个(必要)点为根时,简化树上最大深度点的深度(问题转化)。
最初提到“尝试使用换根 DP”,于是用 \(f_x\) 表示以 \(x\) 为根的子树中最深节点的深度。那么有 \(f_x = \max_{x \xrightarrow{z}{y}} f_y+z\)。
第二次转移:对于每一条边 \(x \xrightarrow z y\),\(f'_y\) 来自两部分,(1)原本的子树 \(f_y\),(2)把 \(x\) 看作 \(y\) 的子树后新的贡献 \(f^*_x\)——\(x\) 除去 \(y\) 后的后代最大深度。
核心是如何从 \(f^*_x\) 中排除 \(y\) 的影响,我先后使用了两种方法:(1)事先将 \(x\) 的所有子树按照 \(f\) 值从大到小排序,如果 \(y\) 为第一个子节点则取第二个子节点(如果该节点存在)的 \(f\) 值;(2)将 \(f\) 改为同时处理最大和次大值,然后将 \(f_y\) 与最大值判断以确定最大值是否由其转移而来,否则取最大值,是则取次大值。
然后我们就获得了所有必要点的 \(f'\) 值,\((2\sum z) - f'_x\) 即为 \(x\) 节点上的答案。
同上几题,\(f\) 和 \(f'\) 也都可以压成一个数组。
III. 非必要点
非必要点的策略很简单:一直走到必要点上,然后像正常点那样走,答案也就是这个必要点的答案加上额外走的距离。
一共需要四个 DFS,简化树一个,必要点换根 DP 两个,非必要点统计答案一个。不过前两个和最后两个 DFS 似乎可以分别压在一起。
本题总体思路:联想到结论,简化树,尽量把所求值往结论上靠→成功转化题意,将所求答案分成两类,换根 DP 求出其中一类,另一类随便乱搞。
P3647 [APIO2014] 连珠线
好题,当赏👍。
状态表示
样例图示:
首先这个游戏构造出的一定是一个树。
因为游戏要求是“从一个珠子开始,每次会用如下方式添加一个新的珠子”,也就是说每次然后如果把第一个点作为树的根节点,那么 Append
操作只能给父节点添加子节点,Insert
操作只能在父子节点间进行。
最后同一次操作中构造出的蓝线一定是以连接“父—子—孙”的形式存在。
当在树上一条链上走的时候,这个东西有点像奇偶变换:父/孙→子→父/孙→子→……(孙子节点同时是下一段链的父节点)。所以初步估计要设计的状态有两层:\(f_{x,0}\) 表示 \(x\) 作为父/孙节点时的答案,\(f_{x,1}\) 表示其作为子节点时的答案,两者互相转换。
后来我在题解区找到了一个意义相同但更加准确的表达:\(f_{x,0}\) 表示 \(x\) 作为端点时的最大的分,\(f_{x,1}\) 表示 \(x\) 作为中点时的最大得分。
状态转移
当一个节点作为父/孙节点的时候,它可以作为任意多条蓝链的端点,所以 \(f_{x,0}=\sum (f_{y,1}+z)\)。
但是这个转移方程并不完善,因为有可能 \(y\) 不能作为链中点(比如说 \(y\) 没有子树的情况)。按照 DP 的惯例,\(f_{y,1}\) 应该等于 \(-\infin\),\(f_{x,0}\) 也就变成 \(-\infin\) 了;还有另外一种可能,那就是 \(y\) 作为父/孙节点时赚的更多。
综上所述,DP 的时候绝不能轻易省略转移来源。设 \(s(y)=\max\{f_{y,1}+z,f_{y,0}\}\),更完整的方程应该是 \(f_{x,0} = \sum s(y)\)。
然后来考虑当 \(x\) 作为子节点时如何转移。\(x\) 作为链中点的时候,后面必须要跟至少一个端点 \(y_0\),那么方程如下:
但是这个方程太恶臭了,观察发现后面的 \(\sum_{y \neq y_0} \max\{ f_{y,1}+z,f_{y,0} \}\) 其实就是 \(f_{x,0}-\max\{ f_{y_0,1}+z,f_{y_0,0} \}\),其中 \(f_{x,0}\) 是定值,可以提到外面去,于是有:
这样就可以快速转移了。
换根 DP
但是,上面的一切推论都是建立在第一个点(也就是树根)确定的基础上,然而本题中树根并不确定,所以可以尝试用换根 DP 统计出每一个点作为根的答案,再取最大值作为答案。
利用“父子交换贡献”的思想,考虑 \(x \rightarrow y\) 中 \(x\) 成为 \(y\) 的儿子以后发生的事情,分别考虑 \(f_{y,0}\) 和 \(f_{y,1}\)。
对于 \(f_{y,0}\),它新增了 \(\max\{f^*_{x,1}+z,f^*_{x,0}\}\)(以下简称 \(s^*(x)\))这一部分(\(f^*\) 表示除去 \(y\) 的贡献);而对于 \(f_{y,1}\),它内部的组成部分需要和 \(s^*(x)\) 再次作比较(把这个组成部分抽出来有两种方法:(1)之前记录 \(f_{y,1}\) 的时候就先不加上 \(f_{y,0}\),要用的时候随用随加;(2)先减去 \(f_{x,0}\),比较更新完再加回来)。
(下面默认采用第一种方法,修改 \(f_{x,1}\) 的定义为原 \(f_{x,1}-f_{x,0}\),即 \(\max\{f_{y,0}-s(y)\}\)。)
所以核心是找到 \(f^*_{x,0}\) 和 \(f^*_{x,1}\),即关键是如何除去 \(f_x\) 中 \(y\) 的贡献。
根据上面的转移方程,\(f_{x,0}\) 是一个累加和,所以直接减去 \(s(y)\) 就能得到 \(f^*_{x,0}\);而 \(f_{x,1}\) 是一个最大值,要想除去 \(y\) 的贡献,根据上面那道题,也有两种方法(排序或记录次大值),于是成功得到了 \(f^*_{x,1}\)。
综上所述:
由上几题,\(f'\) 和 \(f\) 仍然可以压在一起,用一个数组储存,不再赘述。
答案即为 \(\max\{f_{x,0}\}\)。
评价
一道正宗的纯正换根 DP 题,换根 DP 的常用技巧都囊括其中了,思维难度不低,代码和题面也不恶心人,也没有其它什么乱七八糟的算法喧宾夺主。
总结
主要步骤
-
(预处理等操作)
-
第一次 DP(固定根)
- 求解以某一个固定点为根时的子问题
- 同时可以顺便求出其它需要用到的数据
-
第二次 DP(换根)
- (临时)消除该子节点对父节点的贡献
- 将这个父节点的贡献合并到子节点上
-
(尾处理等操作)
两次 DP 一般都采用 DFS 的方式(所以换根 DP 的代码里面通常都可以看到 DFS1
和 DFS2
😁)
第二次 DP 的转移方程可以用两个思想考虑:
- 节点沿边移动:当前节点从 \(x\) 移动到 \(y\) 会发生什么?答案会发生哪些变化?
- 父子交换贡献:当 \(x\) 从 \(y\) 的父节点变成 \(y\) 的子节点后会发生什么?如何消除 \(y\) 对 \(x\) 的贡献?如何再把这个贡献合并到 \(y\) 上面?
一般来说,简单或偏模板一点的题常用第一种思想,而较难的题用第二种思想更好解。
典型特征
-
对每个点都要求出对应的答案且数据范围明显不支持 \(O(n^2)\) 级别算法——几乎已经把“换根 DP”几个字贴到脸上了,\(90\%\) 都是换根 DP。
或者说要求求出以哪一个点作为为起点、终点或其它什么特殊点时答案最值,这个算是上面问法的变式。
-
以某一个点为根时情况确定且这一部分子问题可做——很有可能是换根 DP,建议尝试一下能否换根转移。
这种问题一般需要挖掘一些性质,发现子问题答案只和选择哪一个点作为特殊点有关。或者说确定某一个点为特殊点以后可以在正确的复杂度之下求解这一部分的子问题。
准确来说,第一种特征属于第二种特征,只是问法更加直白,难度也会相应的较第二种更低。
可解条件
- 问题在确定根节点的情况下可解(第一次 DP)。
- 问题可进行换根转移(第二次 DP)。
条件 \(1\) 不属于换根 DP 的讨论范围,重点说条件 \(2\):
根据【主要步骤】部分,换根 DP 第二次 DP 主要分为“消除该子节点对父节点的贡献”和“将这个父节点的贡献合并到子节点上”两个步骤。
既然第一次 DP 可以完成,即可以用子节点的答案合并出父节点的答案,那么第二步(额外合并一个贡献)一般都没有问题。核心是第一步(消除该子节点对父节点的贡献)。
首先,可逆的运算一定可以消除贡献,例如异或(异或消除)、加法(减法消除)、乘法(逆元或除法消除)。
其次,不可逆的运算部分一定程度上也能消除贡献,例如最值问题(通过记录次大次小值消除)。
最后,【例题 P3047】中提到的那种奇技淫巧似乎可以解决一切不可逆运算消除贡献的问题,这里整理归纳一下,过程如下:
- 用 \(\otimes\) 表示某种合并运算,设 \(x\) 的子节点分别为 \(y_1,y_2,\dots,y_k\)。
- 在第一次 DP 的时候记录这些子节点的前缀合并值 \(fl_i= f_{y_1} \otimes f_{y_2} \otimes \dots \otimes f_{y_i}\)、后缀合并值 \(fr_i= f_{y_i} \otimes f_{y_{i+1}} \otimes \dots \otimes f_{y_k}\)。
- 在换根 DP 过程中记录由父亲转移过来的那部分答案 \(up_x\),那么就有 \(up_{y_i}=fl_{i-1} \otimes fr_{i+1}\),\(f'_{y_i}=f_{y_i} \otimes up_{y_i}\)。
- 总体时间复杂度不变。
(不过这种方法依然要求满足结合律。)
其它细节
正常来说,推导换根 DP 状态转移方程的时候是需要将第一次 DP 的结果(记为 \(f_x\))和第二次 DP 的结果(记为 \(ans_x\))分开计算的,但是某些情况下,这两个可以合并到一起:
- \(ans_x\) 在把它换做根以前就已经确定;
- \(ans_y\) 可以由已消除 \(y\) 贡献的 \(ans_x\) 直接合并上已有的 \(f_y\) 得到;
- 如果 \(f\) 或 \(ans\) 包含多个状态,在由 \(x\) 向 \(y\) 转移的过程中可以以某种方式不互相影响或正确影响而转移(见【例题 P3047】)。
总体来说,大多数题目的 \(f\) 和 \(ans\) 都是可以压在一起计算的,即换根的时候可以直接在 \(f\) 上做修改。
本文采用 「CC-BY-NC 4.0」 创作共享协议,转载请注明作者及出处,禁止商业使用。