leetcode
leetcode 2001 ~ 2050
从树中删除边的最小分数

从树中删除边的最小分数

难度:

标签:

题目描述

存在一棵无向连通树,树中有编号从 0n - 1n 个节点, 以及 n - 1 条边。

给你一个下标从 0 开始的整数数组 nums ,长度为 n ,其中 nums[i] 表示第 i 个节点的值。另给你一个二维整数数组 edges ,长度为 n - 1 ,其中 edges[i] = [ai, bi] 表示树中存在一条位于节点 aibi 之间的边。

删除树中两条 不同 的边以形成三个连通组件。对于一种删除边方案,定义如下步骤以计算其分数:

  1. 分别获取三个组件 每个 组件中所有节点值的异或值。
  2. 最大 异或值和 最小 异或值的 差值 就是这一种删除边方案的分数。
  • 例如,三个组件的节点值分别是:[4,5,7][1,9][3,3,3] 。三个异或值分别是 4 ^ 5 ^ 7 = 61 ^ 9 = 83 ^ 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遍历中,如何确保不会重复访问已经访问过的节点?
在DFS遍历中,我们通过使用一个额外的参数`fa`(表示父节点)来确保每个节点不会被重复访问。在遍历节点`x`的邻接节点`y`时,我们会检查`y`是否等于`fa`。如果不等于`fa`,则表示`y`是`x`的一个未访问过的子节点,我们可以安全地递归调用`dfs(y, x)`。这种方法可以有效地防止向父节点回溯,从而避免重复访问节点。
🦆
你是如何使用`in_`和`out`数组来快速确定任意两条边的删除将如何分割树的?具体的逻辑是什么?
在DFS过程中,`in_`数组记录了每个节点的进入时间,而`out`数组记录了每个节点的离开时间。这两个时间戳可以用来判断节点的包含关系。如果节点i的进入时间和离开时间之间包含了节点j的进入时间(`in_[i] < in_[j] <= out[i]`),则节点j在节点i的子树中。这样,如果删除连接i和j的边,树将被分割成两部分:节点j的子树和包含其他所有节点的其余部分。通过这种方式,我们可以快速判断删除任意两条边后树的分割情况,进而计算分割后各部分的异或值。
🦆
为什么选择异或操作来计算每个组件的节点值?使用异或有什么特别的优势吗?
异或操作具有几个特别的优势:首先,异或操作是可交换的和可结合的,这意味着元素的异或可以在任意顺序下进行,而结果不会改变。其次,异或操作满足`x ^ x = 0`和`x ^ 0 = x`的性质,这使得我们能够方便地撤销或添加节点值。在子树计算中,我们可以通过异或父节点的子树结果和当前节点值来更新异或总和。这种属性使得异或操作成为计算组合节点值的理想选择,尤其适用于需要频繁分割和合并节点值的场景,如本题的树分割问题。
🦆
在双层循环中检查边的组合时,为什么从`i = 2`和`j = 1`开始?这样的选择有什么特定的原因吗?
在检查边的组合时,选择从`i = 2`和`j = 1`开始是为了避免重复检查边的组合并确保i和j代表不同的节点。由于在无向图中边是无序的,我们通过固定i和j的开始值和范围来避免重复计算相同的边对(例如,`[1,2]`与`[2,1]`在本题中应被视为相同)。选择这种遍历顺序是为了有效地覆盖所有需要考虑的边对组合,同时确保每个组合只被计算一次。

相关问题