书接上文
树上倍增的具体实现
其实树上倍增大致可以分为三个步骤。
- dfs 预处理倍增数组设
d
p
[
x
]
[
i
]
dp[x][i]
dp[x][i] 表示
x
x
x 点向上跳
2
i
2^i
2i 到达的点,初始值
d
p
[
x
]
[
0
]
dp[x][0]
dp[x][0] 的值就为
x
x
x 的父亲节点的编号,在从根节点往下遍历的过程中,我们已经得到了该点上方所有点的倍增信息,可以得到状态转移方程:
d p [ x ] [ i ] = d p [ d p [ x ] [ i − 1 ] [ i − 1 ] ; dp[x][i]=dp[dp[x][i-1][i-1]; dp[x][i]=dp[dp[x][i−1][i−1];
for (int j = 1; (1 << j) <= deep[x] - 1; j++) {
dp[x][j] = dp[dp[x][j - 1]][j - 1];
}
- 调整两点深度对于深度不同的点,找最近公共祖先比较麻烦,所以我们先把两点的深度统一,即让深度较深的点跳到和深度较浅的点同一深度。具体跳跃方法可以参考差值的二进制数,比如 ( 10110 ) 2 (10110)_2 (10110)2 ,就需要向上跳 2 1 , 2 2 , 2 4 2^1,2^2,2^4 21,22,24。换言之就是,找当前数值中最大的 2 k 2^k 2k 的数,找到后再减去这个数,然后继续找。
if (deep[x] < deep[y]) swap(x, y);
int index = __lg(deep[x] - deep[y]);
for (int i = index; i >= 0; i--) {
if (deep[dp[x][i]] >= deep[y])x = dp[x][i];
if (deep[x] == deep[y])break;
}
- 找最近公共祖先对于深度相同的两点
u
u
u ,
v
v
v ,他们的最近公共祖先可能是任意一个点,那么如何跳跃呢?我们观察这个两个点的
d
p
dp
dp 数组可以发现,在
i
i
i 比较大的时候,
d
p
[
u
]
[
i
]
=
d
p
[
v
]
[
i
]
dp[u][i]=dp[v][i]
dp[u][i]=dp[v][i] ,说明此时已经跳到了最近公共祖先上方,我们需要找到
d
p
[
u
]
[
i
]
!
=
d
p
[
v
]
[
i
]
dp[u][i]!=dp[v][i]
dp[u][i]!=dp[v][i]
由于我们也不知道i的具体值,所以只能一个一个尝试,当树的深度最大为 1 0 5 10^5 105 时, i i i 的最大值为17即可, i i i 的值和数据范围 有关,一般不会超过20,如果发现 d p [ u ] [ i ] = = d p [ v ] [ i ] dp[u][i]==dp[v][i] dp[u][i]==dp[v][i],则不动,否则就同时跳到 d p [ x ] [ i ] dp[x][i] dp[x][i] 这个点,由于一个数一定对应一个二进制数,所以最终一定会跳到最近公共祖先。
for (int j = 18; j >= 0; j--) {
if (dp[x][j] == dp[y][j]) continue;
x = dp[x][j], y = dp[y][j];
}
return dp[x][0];
Tips:
对于边权为1的树,找到两点的最近公共祖先后,通过两点的深度差相减就能得到答案。对于边权不为1的树,虽然也是通过深度差相减得到答案,但是要注意,如果 deep 数组中存储的是该点到根节点的距离,那么在树上倍增第二步操作中,把两个点移动到相同深度的操作就是错误的,这里的深度指的是离根节点经过的节点数量,不是离根节点的距离,所以对于有边权的树,我们需要重新开一个距离数组 dis ,而不能直接修改 deep 数组,这两个在该代码中的作用是不一样的。
小结
树上倍增经常用来求解需要找最近公共祖先的问题,同样,对于序列问题,很多时候还需要求极值,树上倍增也可以用来处理树上路径的极值。处理办法和倍增求区间极值有一点区别,因为对于树上的每一个点,有唯一的父亲节点,但是儿子节点有很多,没有办法记录祖先节点向下找
2
k
2^k
2k 这个区间内的答案。
那么如何求解
u
u
u 点到最近公共祖先
x
x
x 之间的极值呢?先维护一个极值的倍增数组,
m
a
x
[
x
]
[
i
]
max[x][i]
max[x][i] 表示
x
x
x 点到
x
x
x 向上移动
2
i
2^i
2i 这个区间的极值,在
u
u
u 点往上跳的过程中,同时更新极值即可。