Hard
题目描述
给你一个整数 n。存在一棵完全二叉树,有 2^n - 1 个节点。该树的根节点值为 1,对于值为 val 的每个节点(val 在 [1, 2^(n-1) - 1] 范围内),都有两个子节点:
- 左子节点的值为 2 * val
- 右子节点的值为 2 * val + 1
还给你一个长度为 m 的二维整数数组 queries,其中 queries[i] = [ai, bi]。对于每个查询,请解决以下问题:
- 在值为 ai 和 bi 的节点之间添加一条边
- 找到图中环的长度
- 删除值为 ai 和 bi 的节点之间添加的边
注意:
- 环是从同一节点开始和结束的路径,路径中的每条边只访问一次
- 环的长度是环中访问的边数
- 添加查询边后,树中两个节点之间可能有多条边
返回一个长度为 m 的数组 answer,其中 answer[i] 是第 i 个查询的答案。
示例 1:
输入:n = 3, queries = [[5,3],[4,7],[2,3]]
输出:[4,5,3]
解释:图中显示了有 2^3 - 1 个节点的树。红色节点描述了添加边后环中的节点。
- 添加节点 3 和 5 之间的边后,图包含节点 [5,2,1,3] 的环。因此第一个查询的答案是 4。删除添加的边并处理下一个查询。
- 添加节点 4 和 7 之间的边后,图包含节点 [4,2,1,3,7] 的环。因此第二个查询的答案是 5。删除添加的边并处理下一个查询。
- 添加节点 2 和 3 之间的边后,图包含节点 [2,1,3] 的环。因此第三个查询的答案是 3。删除添加的边。
示例 2:
输入:n = 2, queries = [[1,2]]
输出:[2]
解释:图中显示了有 2^2 - 1 个节点的树。红色节点描述了添加边后环中的节点。
- 添加节点 1 和 2 之间的边后,图包含节点 [2,1] 的环。因此第一个查询的答案是 2。删除添加的边。
约束:
- 2 <= n <= 30
- m == queries.length
- 1 <= m <= 10^5
- queries[i].length == 2
- 1 <= ai, bi <= 2^n - 1
- ai != bi
解题思路
这道题的关键在于理解完全二叉树的结构特性和环的形成机制。
核心观察:
- 在完全二叉树中,当我们在两个节点 a 和 b 之间添加一条边时,形成的环路径为:a → LCA(a,b) → b → a
- 环的长度 = dist(a, LCA) + dist(b, LCA) + 1(添加的边)
解题思路:
找最低公共祖先(LCA):在完全二叉树中,节点编号有特殊性质:
- 节点 x 的父节点是 x//2
- 我们可以通过不断向上追溯到根节点来找到两个节点的 LCA
距离计算:
- 节点到根的深度 = log2(node) 向下取整
- 两节点间距离 = depth(a) + depth(b) - 2*depth(LCA)
优化的 LCA 算法:
- 同时从两个节点向上移动,直到它们相遇
- 每次将较大的节点值除以 2(向父节点移动)
- 移动次数就是到 LCA 的距离
算法步骤:
- 对于每个查询 [a, b]
- 使用双指针同时从 a 和 b 向根节点移动
- 记录移动步数,直到两个指针相遇(找到 LCA)
- 环长度 = 总移动步数 + 1
这种方法时间复杂度为 O(log n),因为树的高度最多为 log(2^n) = n。
代码实现
class Solution {
public:
vector<int> cycleLengthQueries(int n, vector<vector<int>>& queries) {
vector<int> result;
for (auto& query : queries) {
int a = query[0], b = query[1];
int steps = 1; // 添加的边
// 找到LCA并计算距离
while (a != b) {
if (a > b) {
a /= 2;
} else {
b /= 2;
}
steps++;
}
result.push_back(steps);
}
return result;
}
};
class Solution:
def cycleLengthQueries(self, n: int, queries: List[List[int]]) -> List[int]:
result = []
for a, b in queries:
steps = 1 # 添加的边
# 找到LCA并计算距离
while a != b:
if a > b:
a //= 2
else:
b //= 2
steps += 1
result.append(steps)
return result
public class Solution {
public int[] CycleLengthQueries(int n, int[][] queries) {
int[] result = new int[queries.Length];
for (int i = 0; i < queries.Length; i++) {
int a = queries[i][0], b = queries[i][1];
int steps = 1; // 添加的边
// 找到LCA并计算距离
while (a != b) {
if (a > b) {
a /= 2;
} else {
b /= 2;
}
steps++;
}
result[i] = steps;
}
return result;
}
}
var cycleLengthQueries = function(n, queries) {
const result = [];
for (const [a, b] of queries) {
let nodeA = a, nodeB = b;
let steps = 1; // 添加的边
// 找到LCA并计算距离
while (nodeA !== nodeB) {
if (nodeA > nodeB) {
nodeA = Math.floor(nodeA / 2);
} else {
nodeB = Math.floor(nodeB / 2);
}
steps++;
}
result.push(steps);
}
return result;
};
复杂度分析
| 复杂度类型 | 大小 |
|---|---|
| 时间复杂度 | O(m × n),其中 m 是查询数量,n 是树的深度,每次查询需要 O(n) 时间找到 LCA |
| 空间复杂度 | O(1),只使用常数额外空间(不计算输出数组) |