Hard
题目描述
给定一个有 n 个节点的无向树,节点标号从 0 到 n - 1。给定整数 n 和长度为 n - 1 的二维整数数组 edges,其中 edges[i] = [ui, vi, wi] 表示节点 ui 和 vi 之间有一条权重为 wi 的边。
还给定一个长度为 m 的二维整数数组 queries,其中 queries[i] = [ai, bi]。对于每个查询,找到使从 ai 到 bi 路径上每条边的权重都相等所需的最小操作数。在一次操作中,你可以选择树的任意一条边并将其权重改为任意值。
注意:
- 查询彼此独立,意味着每个新查询时树都会回到初始状态
- 从 ai 到 bi 的路径是一个不同节点的序列,以节点 ai 开始,以节点 bi 结束,序列中每两个相邻节点在树中共享一条边
返回长度为 m 的数组 answer,其中 answer[i] 是第 i 个查询的答案。
示例 1:
输入: n = 7, edges = [[0,1,1],[1,2,1],[2,3,1],[3,4,2],[4,5,2],[5,6,2]], queries = [[0,3],[3,6],[2,6],[0,6]]
输出: [0,0,1,3]
示例 2:
输入: n = 8, edges = [[1,2,6],[1,3,4],[2,4,6],[2,5,3],[3,6,6],[3,0,8],[7,0,2]], queries = [[4,6],[0,4],[6,5],[7,4]]
输出: [1,2,2,3]
约束:
- 1 <= n <= 10^4
- edges.length == n - 1
- edges[i].length == 3
- 0 <= ui, vi < n
- 1 <= wi <= 26
- 输入保证 edges 表示有效的树
- 1 <= queries.length == m <= 2 * 10^4
- queries[i].length == 2
- 0 <= ai, bi < n
解题思路
这道题目的核心思路是:对于路径上的所有边,我们需要统计每种权重出现的次数,然后选择出现次数最多的权重作为目标权重,其他边都需要修改,操作次数就是总边数减去最多权重的边数。
解题步骤:
构建图和预处理:使用邻接表构建无向树,并进行 DFS 预处理,记录每个节点到根节点路径上各种权重的出现次数。
LCA 算法:使用二分提升(Binary Lifting)算法快速计算最近公共祖先,这样可以高效地处理查询。
路径权重统计:对于查询 [a, b],路径上权重 w 的出现次数等于
freq[a][w] + freq[b][w] - 2 * freq[lca(a,b)][w]。这是因为 LCA 到根的路径被重复计算了两次。计算最小操作数:统计路径上所有权重的频次,找出最大频次,总边数减去最大频次即为答案。
由于边权重范围是 1-26,可以用数组直接记录每种权重的频次,时间复杂度很低。
推荐解法:使用二分提升 + 路径频次统计的方法,能够高效处理大量查询。
代码实现
class Solution {
public:
vector<int> minOperationsQueries(int n, vector<vector<int>>& edges, vector<vector<int>>& queries) {
// 构建邻接表
vector<vector<pair<int, int>>> graph(n);
for (auto& edge : edges) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].push_back({v, w});
graph[v].push_back({u, w});
}
// 二分提升预处理
int logN = 15; // log2(10^4) < 15
vector<vector<int>> parent(n, vector<int>(logN, -1));
vector<int> depth(n);
vector<vector<int>> freq(n, vector<int>(27, 0)); // 权重范围1-26
// DFS 预处理
function<void(int, int, int)> dfs = [&](int u, int p, int d) {
parent[u][0] = p;
depth[u] = d;
// 二分提升预处理
for (int i = 1; i < logN && parent[u][i-1] != -1; i++) {
parent[u][i] = parent[parent[u][i-1]][i-1];
}
for (auto& [v, w] : graph[u]) {
if (v != p) {
// 继承父节点的频次并加上当前边的权重
freq[v] = freq[u];
freq[v][w]++;
dfs(v, u, d + 1);
}
}
};
dfs(0, -1, 0);
// LCA函数
auto lca = [&](int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
// 将u提升到与v相同深度
int diff = depth[u] - depth[v];
for (int i = 0; i < logN; i++) {
if ((diff >> i) & 1) {
u = parent[u][i];
}
}
if (u == v) return u;
// 同时向上提升直到找到LCA
for (int i = logN - 1; i >= 0; i--) {
if (parent[u][i] != parent[v][i]) {
u = parent[u][i];
v = parent[v][i];
}
}
return parent[u][0];
};
vector<int> result;
for (auto& query : queries) {
int a = query[0], b = query[1];
int lca_node = lca(a, b);
// 计算路径上每种权重的频次
vector<int> pathFreq(27, 0);
int totalEdges = 0;
for (int w = 1; w <= 26; w++) {
pathFreq[w] = freq[a][w] + freq[b][w] - 2 * freq[lca_node][w];
totalEdges += pathFreq[w];
}
// 找最大频次
int maxFreq = 0;
for (int w = 1; w <= 26; w++) {
maxFreq = max(maxFreq, pathFreq[w]);
}
result.push_back(totalEdges - maxFreq);
}
return result;
}
};
class Solution:
def minOperationsQueries(self, n: int, edges: List[List[int]], queries: List[List[int]]) -> List[int]:
from collections import defaultdict
# 构建邻接表
graph = defaultdict(list)
for u, v, w in edges:
graph[u].append((v, w))
graph[v].append((u, w))
# 二分提升预处理
logN = 15
parent = [[-1] * logN for _ in range(n)]
depth = [0] * n
freq = [[0] * 27 for _ in range(n)] # 权重范围1-26
def dfs(u, p, d):
parent[u][0] = p
depth[u] = d
# 二分提升预处理
for i in range(1, logN):
if parent[u][i-1] != -1:
parent[u][i] = parent[parent[u][i-1]][i-1]
else:
break
for v, w in graph[u]:
if v != p:
# 继承父节点的频次并加上当前边的权重
freq[v] = freq[u][:]
freq[v][w] += 1
dfs(v, u, d + 1)
dfs(0, -1, 0)
def lca(u, v):
if depth[u] < depth[v]:
u, v = v, u
# 将u提升到与v相同深度
diff = depth[u] - depth[v]
for i in range(logN):
if (diff >> i) & 1:
u = parent[u][i]
if u == v:
return u
# 同时向上提升直到找到LCA
for i in range(logN - 1, -1, -1):
if parent[u][i] != parent[v][i]:
u = parent[u][i]
v = parent[v][i]
return parent[u][0]
result = []
for a, b in queries:
lca_node = lca(a, b)
# 计算路径上每种权重的频次
pathFreq = [0] * 27
totalEdges = 0
for w in range(1, 27):
pathFreq[w] = freq[a][w] + freq[b][w] - 2 * freq[lca_node][w]
totalEdges += pathFreq[w]
# 找最大频次
maxFreq = max(pathFreq[1:27])
result.append(totalEdges - maxFreq)
return result
public class Solution {
public int[] MinOperationsQueries(int n, int[][] edges, int[][] queries) {
// 构建邻接表
List<List<(int, int)>> graph = new List<List<(int, int)>>();
for (int i = 0; i < n; i++) {
graph.Add(new List<(int, int)>());
}
foreach (var edge in edges) {
int u = edge[0], v = edge[1], w = edge[2];
graph[u].Add((v, w));
graph[v].Add((u, w));
}
// 二分提升预处理
int logN = 15;
int[,] parent = new int[n, logN];
int[] depth = new int[n];
int[,] freq = new int[n, 27]; // 权重范围1-26
// 初始化parent为-1
for (int i = 0; i < n; i++) {
for (int j = 0; j < logN; j++) {
parent[i, j] = -1;
}
}
// DFS预处理
void dfs(int u, int p, int d) {
parent[u, 0] = p;
depth[u] = d;
// 二分提升预处理
for (int i = 1; i < logN && parent[u, i-1] != -1; i++) {
parent[u, i] = parent[parent[u, i-1], i-1];
}
foreach (var (v, w) in graph[u]) {
if (v != p) {
// 继承父节点的频次并加上当前边的权重
for (int i = 0; i < 27; i++) {
freq[v, i] = freq[u, i];
}
freq[v, w]++;
dfs(v, u, d + 1);
}
}
}
dfs(0, -1, 0);
// LCA函数
int lca(int u, int v) {
if (depth[u] < depth[v]) {
int temp = u; u = v; v = temp;
}
// 将u提升到与v相同深度
int diff = depth[u] - depth[v];
for (int i = 0; i < logN; i++) {
if ((diff >> i & 1) == 1) {
u = parent[u, i];
}
}
if (u == v) return u;
// 同时向上提升直到找到LCA
for (int i = logN - 1; i >= 0; i--) {
if (parent[u, i] != parent[v, i]) {
u = parent[u, i];
v = parent[v, i];
}
}
return parent[u, 0];
}
int[] result = new int[queries.Length];
for (int i = 0; i < queries.Length; i++) {
int a = queries[i][0], b = queries[i][1];
int lca_node = lca(a, b);
// 计算路径上每种权重的频次
int[] pathFreq = new int[27];
int totalEdges = 0;
for (int w = 1; w <= 26; w++) {
pathFreq[w] = freq[a, w] + freq[b, w] - 2 * freq[lca_node, w];
totalEdges += pathFreq[w];
}
// 找最大频次
int maxFreq = 0;
for (int w = 1; w <= 26; w++) {
maxFreq = Math.Max(maxFreq, pathFreq[w]);
}
result[i] = totalEdges - maxFreq;
}
return result;
}
}
var minOperationsQueries = function(n, edges, queries) {
const graph = Array(n).fill().map(() => []);
for (const [u, v, w] of edges) {
graph[u].push([v, w]);
graph[v].push([u, w]);
}
const LOG = Math.ceil(Math.log2(n)) + 1;
const parent = Array(n).fill().map(() => Array(LOG).fill(-1));
const depth = Array(n).fill(0);
const weightCount = Array(n).fill().map(() => Array(27).fill(0));
function dfs(node, par, d) {
parent[node][0] = par;
depth[node] = d;
for (let i = 1; i < LOG; i++) {
if (parent[node][i - 1] !== -1) {
parent[node][i] = parent[parent[node][i - 1]][i - 1];
}
}
for (const [child, weight] of graph[node]) {
if (child !== par) {
for (let i = 1; i <= 26; i++) {
weightCount[child][i] = weightCount[node][i];
}
weightCount[child][weight]++;
dfs(child, node, d + 1);
}
}
}
dfs(0, -1, 0);
function lca(u, v) {
if (depth[u] < depth[v]) [u, v] = [v, u];
const diff = depth[u] - depth[v];
for (let i = 0; i < LOG; i++) {
if ((diff >> i) & 1) {
u = parent[u][i];
}
}
if (u === v) return u;
for (let i = LOG - 1; i >= 0; i--) {
if (parent[u][i] !== parent[v][i]) {
u = parent[u][i];
v = parent[v][i];
}
}
return parent[u][0];
}
const result = [];
for (const [a, b] of queries) {
const lcaNode = lca(a, b);
const pathLength = depth[a] + depth[b] - 2 * depth[lcaNode];
let maxCount = 0;
for (let w = 1; w <= 26; w++) {
const count = weightCount[a][w] + weightCount[b][w] - 2 * weightCount[lcaNode][w];
maxCount = Math.max(maxCount, count);
}
result.push(pathLength - maxCount);
}
return result;
};
复杂度分析
| 指标 | 复杂度 |
|---|---|
| 时间 | - |
| 空间 | - |