从树中删除边的最小分数
难度:
标签:
题目描述
存在一棵无向连通树,树中有编号从 0
到 n - 1
的 n
个节点, 以及 n - 1
条边。
给你一个下标从 0 开始的整数数组 nums
,长度为 n
,其中 nums[i]
表示第 i
个节点的值。另给你一个二维整数数组 edges
,长度为 n - 1
,其中 edges[i] = [ai, bi]
表示树中存在一条位于节点 ai
和 bi
之间的边。
删除树中两条 不同 的边以形成三个连通组件。对于一种删除边方案,定义如下步骤以计算其分数:
- 分别获取三个组件 每个 组件中所有节点值的异或值。
- 最大 异或值和 最小 异或值的 差值 就是这一种删除边方案的分数。
- 例如,三个组件的节点值分别是:
[4,5,7]
、[1,9]
和[3,3,3]
。三个异或值分别是4 ^ 5 ^ 7 = 6
、1 ^ 9 = 8
和3 ^ 3 ^ 3 = 3
。最大异或值是8
,最小异或值是3
,分数是8 - 3 = 5
。
返回在给定树上执行任意删除边方案可能的 最小 分数。
示例 1:

输入:nums = [1,5,5,4,11], edges = [[0,1],[1,2],[1,3],[3,4]] 输出:9 解释:上图展示了一种删除边方案。 - 第 1 个组件的节点是 [1,3,4] ,值是 [5,4,11] 。异或值是 5 ^ 4 ^ 11 = 10 。 - 第 2 个组件的节点是 [0] ,值是 [1] 。异或值是 1 = 1 。 - 第 3 个组件的节点是 [2] ,值是 [5] 。异或值是 5 = 5 。 分数是最大异或值和最小异或值的差值,10 - 1 = 9 。 可以证明不存在分数比 9 小的删除边方案。
示例 2:

输入:nums = [5,5,2,4,4,2], edges = [[0,1],[1,2],[5,2],[4,3],[1,3]] 输出:0 解释:上图展示了一种删除边方案。 - 第 1 个组件的节点是 [3,4] ,值是 [4,4] 。异或值是 4 ^ 4 = 0 。 - 第 2 个组件的节点是 [1,0] ,值是 [5,5] 。异或值是 5 ^ 5 = 0 。 - 第 3 个组件的节点是 [2,5] ,值是 [2,2] 。异或值是 2 ^ 2 = 0 。 分数是最大异或值和最小异或值的差值,0 - 0 = 0 。 无法获得比 0 更小的分数 0 。
提示:
n == nums.length
3 <= n <= 1000
1 <= nums[i] <= 108
edges.length == n - 1
edges[i].length == 2
0 <= ai, bi < n
ai != bi
edges
表示一棵有效的树
代码结果
运行时间: 1319 ms, 内存: 17.4 MB
/*
* 思路:
* 1. 使用流操作简化异或值计算和分数计算。
* 2. 遍历所有可能的删除边组合。
* 3. 使用流操作处理图结构。
*/
import java.util.*;
import java.util.stream.Collectors;
public class Solution {
public int minimumScore(int[] nums, int[][] edges) {
List<List<Integer>> graph = new ArrayList<>();
for (int i = 0; i < nums.length; i++) {
graph.add(new ArrayList<>());
}
Arrays.stream(edges).forEach(edge -> {
graph.get(edge[0]).add(edge[1]);
graph.get(edge[1]).add(edge[0]);
});
int[] xor = new int[nums.length];
boolean[] visited = new boolean[nums.length];
dfs(0, -1, graph, nums, xor, visited);
return Arrays.stream(edges)
.flatMapToInt(edge1 -> Arrays.stream(edges).filter(edge2 -> !Arrays.equals(edge1, edge2)).mapToInt(edge2 -> calculateScore(xor, edge1, edge2, graph, nums)))
.min()
.orElse(Integer.MAX_VALUE);
}
private void dfs(int node, int parent, List<List<Integer>> graph, int[] nums, int[] xor, boolean[] visited) {
visited[node] = true;
xor[node] = nums[node];
for (int neighbor : graph.get(node)) {
if (neighbor != parent && !visited[neighbor]) {
dfs(neighbor, node, graph, nums, xor, visited);
xor[node] ^= xor[neighbor];
}
}
}
private int calculateScore(int[] xor, int[] edge1, int[] edge2, List<List<Integer>> graph, int[] nums) {
int node1 = edge1[0], node2 = edge1[1];
int node3 = edge2[0], node4 = edge2[1];
int xor1 = xor[node1], xor2 = xor[node2];
int xor3 = xor[node3], xor4 = xor[node4];
int[] components = {xor1, xor2, xor3, xor4};
Arrays.sort(components);
return components[3] - components[0];
}
}
解释
方法:
该题解的核心思路是使用深度优先搜索(DFS)来计算每个子树的异或总和,并利用这些预计算的值来快速确定三个不同的连通分量的异或值。首先,构建图并通过一次DFS遍历来获得每个节点的进入和离开时间,同时计算从根节点到每个节点的异或路径值。使用这些信息,我们可以快速确定任意两条边的删除将如何分割树,并计算得到的三个分量的异或值。然后,通过尝试所有可能的边对组合来找出产生最小分数差的组合。这种方法避免了对每种可能的边对组合进行完整的DFS搜索,从而显著提高了效率。
时间复杂度:
O(n^2)
空间复杂度:
O(n)
代码细节讲解
🦆
在DFS遍历中,如何确保不会重复访问已经访问过的节点?
▷🦆
你是如何使用`in_`和`out`数组来快速确定任意两条边的删除将如何分割树的?具体的逻辑是什么?
▷🦆
为什么选择异或操作来计算每个组件的节点值?使用异或有什么特别的优势吗?
▷🦆
在双层循环中检查边的组合时,为什么从`i = 2`和`j = 1`开始?这样的选择有什么特定的原因吗?
▷