好路径的数目
难度:
标签:
题目描述
给你一棵 n
个节点的树(连通无向无环的图),节点编号从 0
到 n - 1
且恰好有 n - 1
条边。
给你一个长度为 n
下标从 0 开始的整数数组 vals
,分别表示每个节点的值。同时给你一个二维整数数组 edges
,其中 edges[i] = [ai, bi]
表示节点 ai
和 bi
之间有一条 无向 边。
一条 好路径 需要满足以下条件:
- 开始节点和结束节点的值 相同 。
- 开始节点和结束节点中间的所有节点值都 小于等于 开始节点的值(也就是说开始节点的值应该是路径上所有节点的最大值)。
请你返回不同好路径的数目。
注意,一条路径和它反向的路径算作 同一 路径。比方说, 0 -> 1
与 1 -> 0
视为同一条路径。单个节点也视为一条合法路径。
示例 1:
输入:vals = [1,3,2,1,3], edges = [[0,1],[0,2],[2,3],[2,4]] 输出:6 解释:总共有 5 条单个节点的好路径。 还有 1 条好路径:1 -> 0 -> 2 -> 4 。 (反方向的路径 4 -> 2 -> 0 -> 1 视为跟 1 -> 0 -> 2 -> 4 一样的路径) 注意 0 -> 2 -> 3 不是一条好路径,因为 vals[2] > vals[0] 。
示例 2:
输入:vals = [1,1,2,2,3], edges = [[0,1],[1,2],[2,3],[2,4]] 输出:7 解释:总共有 5 条单个节点的好路径。 还有 2 条好路径:0 -> 1 和 2 -> 3 。
示例 3:
输入:vals = [1], edges = [] 输出:1 解释:这棵树只有一个节点,所以只有一条好路径。
提示:
n == vals.length
1 <= n <= 3 * 104
0 <= vals[i] <= 105
edges.length == n - 1
edges[i].length == 2
0 <= ai, bi < n
ai != bi
edges
表示一棵合法的树。
代码结果
运行时间: 246 ms, 内存: 29.4 MB
/*
题目思路:
1. 使用Java Stream API优化代码,处理相同值节点时使用stream流式操作。
2. 按值升序处理节点,并查集和合并操作同上。
*/
import java.util.*;
import java.util.stream.*;
public class GoodPathCounterStream {
private int[] parent;
private int[] rank;
public int numberOfGoodPaths(int[] vals, int[][] edges) {
int n = vals.length;
parent = new int[n];
rank = new int[n];
Map<Integer, List<Integer>> valToNodes = IntStream.range(0, n)
.boxed()
.collect(Collectors.groupingBy(i -> vals[i]));
Arrays.setAll(parent, i -> i);
Arrays.fill(rank, 1);
int[][] graph = new int[n][n];
for (int[] edge : edges) {
graph[edge[0]][edge[1]] = 1;
graph[edge[1]][edge[0]] = 1;
}
int goodPaths = n;
for (int val : valToNodes.keySet()) {
List<Integer> nodes = valToNodes.get(val);
nodes.forEach(node -> IntStream.range(0, n)
.filter(next -> graph[node][next] == 1 && vals[next] <= val)
.forEach(next -> union(node, next)));
Map<Integer, Long> groupCount = nodes.stream()
.map(this::find)
.collect(Collectors.groupingBy(x -> x, Collectors.counting()));
goodPaths += groupCount.values().stream()
.mapToLong(count -> count * (count - 1) / 2)
.sum();
}
return goodPaths;
}
private int find(int x) {
if (parent[x] != x) {
parent[x] = find(parent[x]);
}
return parent[x];
}
private void union(int x, int y) {
int rootX = find(x);
int rootY = find(y);
if (rootX != rootY) {
if (rank[rootX] > rank[rootY]) {
parent[rootY] = rootX;
} else if (rank[rootX] < rank[rootY]) {
parent[rootX] = rootY;
} else {
parent[rootY] = rootX;
rank[rootX]++;
}
}
}
}
解释
方法:
这个题解使用了一个联合查找(Union-Find)数据结构,用来高效地处理节点的连接和根查找。基本思路是:1. 初始化每个节点为独立的集合。2. 对边进行排序,以边两端节点的最大值为键进行排序。这样可以保证当处理一条边时,路径上较大的值已经处理过,符合好路径定义中的“中间所有节点值都小于等于起始节点值”。3. 通过并查集合并操作来连接节点,如果两个节点属于不同集合且值相同,则这两个集合可以合并,并且此时这两个集合可能的路径数为两个集合大小的乘积。4. 遍历所有边,进行合并操作,并计算可能的好路径。单个节点的好路径(节点自身)在初始化时就加入了总数。
时间复杂度:
O(n log n)
空间复杂度:
O(n)
代码细节讲解
🦆
题解中提到对边按最大节点值排序的目的是什么?这种排序方式对解题有什么具体帮助?
▷🦆
并查集中的`maxv`数组是如何维护的,它在整个解题过程中扮演了什么角色?
▷🦆
在并查集的`union`函数中,为什么要比较`maxv[p1]`和`maxv[p2]`而不是直接合并?这样做有什么优势?
▷🦆
题解中`res += cnt[p1] * cnt[p2]`这一步的计算逻辑是什么?它如何确保不重复计算同一条路径的反向路径?
▷