LeetCode 1483. 树节点的第 K 个祖先
题目描述



题意分析
给定一棵树的父节点数组,需要多次查询从
node连续向上走k条边后到达的祖先;如果途中已经越过根节点,返回-1。每次逐层找父节点需要走
k次。树本身不会变化,可以先记录每个节点向上走1、2、4、8……步的结果,再把查询步数拆成这些二的幂。
解法:倍增预处理祖先表
核心思路
[!blue]
定义
up[v][j]为节点v向上走2^j步后到达的节点,不存在时为-1。第0列走一步,直接使用parent[v];更长的一次跳跃可以拆成两段相同长度的跳跃。计算第
j列时,先令p = up[v][j-1],走完前半段。如果p == -1,说明前半段都无法完成,更长祖先也不存在;否则再从p走2^(j-1)步,得到up[v][j] = up[p][j-1]。预处理必须先算完所有节点的第
j-1列,再计算第j列,因为第二段可能访问任意编号的祖先节点。父节点的编号不保证更小,不能按节点逐行填完整张表。查询时,从低位到高位检查
k的二进制位。第j位为1,就把当前node更新为up[node][j];为0则不动。每次跳跃都从上一次到达的节点继续,处理完所有置位后,总步数恰好等于k。若中途变成-1,更远处也不存在祖先,立即结束即可。题目保证
k <= n。代码令maxLog为满足2^maxLog > n的最小正整数,因此0..maxLog-1列覆盖所有可能用到的二进制位。
解题步骤
- 根据
n算出表的列数,将每个节点的第0列设为直接父节点。- 从小到大枚举列
j,对全部节点合并两段2^(j-1)步跳跃;不存在的祖先统一记录为-1。- 查询时检查
k的各二进制位,置位就从当前节点按对应列跳跃。- 所有位处理完后返回当前节点;若中途已到达
-1,返回-1。
代码实现
class TreeAncestor {
private final int[][] up;
private final int maxLog;
public TreeAncestor(int n, int[] parent) {
int log = 1;
while ((1 << log) <= n) {
log++;
}
maxLog = log;
up = new int[n][maxLog];
for (int i = 0; i < n; i++) {
up[i][0] = parent[i];
}
for (int j = 1; j < maxLog; j++) {
for (int i = 0; i < n; i++) {
// 长跳跃由两段等长短跳跃组成,先找中间祖先。
int p = up[i][j - 1];
if (p == -1) {
// 中间节点不存在,更远祖先也不存在。
up[i][j] = -1;
} else {
up[i][j] = up[p][j - 1];
}
}
}
}
public int getKthAncestor(int node, int k) {
for (int j = 0; j < maxLog && node != -1; j++) {
if (((k >> j) & 1) == 1) {
// k 的第 j 个二进制位为 1,就从当前节点向上跳 2^j 步。
node = up[node][j];
}
}
return node;
}
}
type TreeAncestor struct {
up [][]int
maxLog int
}
func Constructor(n int, parent []int) TreeAncestor {
log := 1
for (1 << log) <= n {
log++
}
up := make([][]int, n)
for i := 0; i < n; i++ {
up[i] = make([]int, log)
up[i][0] = parent[i]
}
for j := 1; j < log; j++ {
for i := 0; i < n; i++ {
// 长跳跃由两段等长短跳跃组成,先找中间祖先。
p := up[i][j-1]
if p == -1 {
// 中间节点不存在,更远祖先也不存在。
up[i][j] = -1
} else {
up[i][j] = up[p][j-1]
}
}
}
return TreeAncestor{up: up, maxLog: log}
}
func (t *TreeAncestor) GetKthAncestor(node int, k int) int {
for j := 0; j < t.maxLog && node != -1; j++ {
if (k>>j)&1 == 1 {
// k 的第 j 个二进制位为 1,就从当前节点向上跳 2^j 步。
node = t.up[node][j]
}
}
return node
}
复杂度分析
- 时间复杂度:预处理为 $O(n\log(n+1))$,每个表格位置只计算一次;单次查询为 $O(\log(n+1))$,最多检查全部列。
- 空间复杂度:$O(n\log(n+1))$,用于保存每个节点的倍增祖先表。
关键点总结
[!green]
- 表的列号是步数的指数,
up[v][j]跳2^j步,而不是跳j步。- 长跳跃依赖两个同长度短跳跃,按列填表才能保证依赖已经准备好。
- 查询中的当前节点持续改变,所选步长之和恰好是目标
k。-1表示祖先不存在,必须先判断,不能把它继续当作数组下标。
易错点总结
[!yellow]
- 用默认值
0表示不存在,会把缺失祖先误认成根节点;根的父节点及更远祖先应为-1。- 没检查中间祖先
p就访问up[p],会在到达根上方时越界。- 每次都从最初的
node跳跃,而不更新当前节点,得到的步数不会累加。- 按节点逐行预处理,可能读取编号更大的父节点尚未计算的列。
- 忽略最高一位的覆盖要求,会漏掉
k中最大的跳跃距离;表宽应满足2^maxLog > n。
相似题目
| 题目 | 难度 | 关联与区别 |
|---|---|---|
| 236. 二叉树的最近公共祖先 | 中等 | 最近公共祖先查询可用倍增先把深度对齐,再同时向上跳;本题提供固定步数祖先查询。 |
| 50. Pow(x, n) | 中等 | 同样按二进制拆分重复次数,本题把祖先关系预合成为2的幂步长。 |
转载与许可
许可
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处!