[树形DP] 换根DP
换根DP
某些树形DP问题中, 我们要求的值是类似 "以当前节点为根节点得到的答案", 却没有给出固定的根节点, 若仍然按照常规的树形DP思路对每个点进行DP, 我们对每一个节点均进行一次 DFS , 最后的复杂度是 \(O\left(n^2\right)\)
如果我们先假设任意一个点为根进行 DP, 求出当前树形结构下以每个点为根的子树的 DP 结果, 然后再将它的结果与父节点的结果一并处理, 也可以得到以它为根节点的整棵树的 DP 结果, 而这么做, 我们只需要进行两次 DFS, 就可以得到答案, 而这么做, 最后的复杂度只有 \(O\left(n\right)\) , 明显优于前者, 我们称这种做法为换根DP.
例题:
CodeForces 219 D Choosing Capital for Treeland CodeForces 洛谷
首先它说每条边是有方向的, 但实际为了方便 DP, 我们可以建双向边, 然后在边上记录当前的边是不是与它自身的指向相反
对于每个节点, 我们可以考虑先求出以某点为根 ( 假设是 \(1\) ) 的 DP 值.
然后我们对每个点的值更新, 方程为:
\[f\left(son\right) = f\left(father\right) \pm 1
\]
当父亲到儿子的边为正向时, 答案 \(+1\), 反之 \(-1\).
**为什么用 "\(=\)" ? **
- 根据第一次DP的树形结构, 当前儿子的答案已经被包含到父亲中了, 所以采用 "\(=\)" .
代码:
# include <iostream>
# include <cstdio>
# define MAXN 2000005
using namespace std;
struct edge{
int v, next;
bool inv; // inv = 0 说明这条边是 u 到 v, = 1 是 v 到 u
}e[MAXN<<1];
int hd[MAXN<<1], cntE;
void AddE(int u, int v, int w){
e[++cntE].v = v, e[cntE].inv = w, e[cntE].next = hd[u], hd[u] = cntE;
}
int f[MAXN], ans; // ans 记录每一轮计算得到的最小值
void Pre(int now, int fa){
for(int i = hd[now]; i; i = e[i].next){
if(e[i].v == fa) continue;
f[now] += e[i].inv;
Pre(e[i].v, now);
f[now] += f[e[i].v];
}
} // 先跑一遍以 i 为根的子树的答案
void DFS(int now, int fa){
ans = min(f[now], ans);
for(int i = hd[now]; i; i = e[i].next){
if(e[i].v == fa) continue;
f[e[i].v] = f[now] + (e[i].inv ? -1 : 1);
DFS(e[i].v, now);
}
} // 再跑一遍以 i 为根的全树的答案
int main(){
int n, u, v;
while(scanf("%d", &n) != EOF){
cntE = 0;
ans = 2e5+1;
for(int i = 1; i < n; i++){
scanf("%d%d", &u, &v);
AddE(u, v, 0); AddE(v, u, 1);
f[i] = 0;
}
f[n] = 0;
Pre(1, 0);
DFS(1, 0);
printf("%d\n", ans);
for(int i = 1; i <= n; i++){
if(f[i] == ans)
printf("%d ", i);
}
printf("\n");
}
return 0;
}