LeetCode 1697 # 检查边长度限制的路径是否存在 由此题而引发的并查集学习
题目来源:Leetcode
题目详情
给你一个 n 个点组成的无向图边集 edgeList ,其中 edgeList[i] = [ui, vi, disi] 表示点 ui 和点 vi 之间有一条长度为 disi 的边。请注意,两个点之间可能有 超过一条边 。
给你一个查询数组queries ,其中 queries[j] = [pj, qj, limitj] ,你的任务是对于每个查询 queries[j] ,判断是否存在从 pj 到 qj 的路径,且这条路径上的每一条边都 严格小于 limitj 。
请你返回一个 布尔数组 answer ,其中 answer.length == queries.length ,当 queries[j] 的查询结果为 true 时, answer 第 j 个值为 true ,否则为 false 。
题目示例
示例 1:
输入:n = 3, edgeList = [[0,1,2],[1,2,4],[2,0,8],[1,0,16]], queries = [[0,1,2],[0,2,5]]
输出:[false,true]
解释:上图为给定的输入数据。注意到 0 和 1 之间有两条重边,分别为 2 和 16 。
对于第一个查询,0 和 1 之间没有小于 2 的边,所以我们返回 false 。
对于第二个查询,有一条路径(0 -> 1 -> 2)两条边都小于 5 ,所以这个查询我们返回 true 。
示例2:
输入:n = 5, edgeList = [[0,1,10],[1,2,5],[2,3,9],[3,4,13]], queries = [[0,4,14],[1,4,13]]
输出:[true,false]
解释:上图为给定数据。
提示:
- 2 <= n <= 105
- 1 <= edgeList.length, queries.length <= 105
- edgeList[i].length == 3
- queries[j].length == 3
- 0 <= ui, vi, pj, qj <= n - 1
- ui != vi
- pj != qj
- 1 <= disi, limitj <= 109
- 两个点之间可能有 多条 边。
前置概念
并查集
对于并查集我的理解是对于需要分组处理的数据,可以利用这个数据结构进行维护
并查集是一个数据结构,他对于各种分组集合,有以下两个操作
- 合并(union)
- 查询(find)
定义
我们以int为例,试维护一群int类型的元素的集合
那么节点值,就是这个元素本身的值,很容易理解
什么是代表节点呢。
代表节点
在初试条件下(假设上图为初始条件),每个元素所代表的集合只有自己,那他的代表节点就是他自己,我代表我自己没问题吧。
进一步,我们操作1节点和4节点合并,两个节点通过某种定义的规则进行合并(具体情况具体分析),假设这边合并之后1为新的集合的代表节点,则生成集合如下图
箭头指向的是代表元素
此时1元素的代表节点还是他自己,4元素的代表节点因为已经合并了(因为一个元素只有一个代表节点),因此变为了1。(注意合并规则不同题目不同讨论)
下一步,2节点所属的集合想要和4节点所属的集合合并 该怎么操作?
我们分别找到两个节点的代表节点,然后根据我们实现定义好的规则合并,这里我假设通过规则后将2并入{1,4},因为{1,4}的代表节点是1,所以将2并入1中,像这样。
这样一个新的集合就合并好了。
现在我们假设3和5也进行了合并
此时我们想把4所代表的集合和5所代表的集合合并,该怎么办。
还是和上面一样分别找到各元素的代表节点,然后根据规则合并。
于是,一个新的集合就变成了这样
其实他的本质就是一颗所有结点都指向根节点的一颗树罢了。
上述过程中,我们只需要用两种操作即可实现,那就是开头说的,查找(Find)和合并(union)
那么代码很容易给出来
java版本
int[] fa;
public void init(int n){
fa = new int[n];
for(int i = 0;i<n;i++){
fa[i] = i;
}
}
public int find(int[] fa,int a){
if(fa[a]==a){
return a;
}else {
fa[a] = find(fa,fa[a]);
return fa[a];
}
}
public void union(int[] fa,int i,int j){
int x = find(fa,i);
int y = find(fa,j);
fa[x] = y;
}
go版本
type UnionSet struct {
fa []int
}
func (u UnionSet) Init(n int) []int {
for i := 1; i <= n; i++ {
u.fa[i] = i
}
return u.fa
}
func (u UnionSet) Union(i int, j int) {
x := u.fa[i]
y:= u.fa[j]
u.fa[x] = y
}
func (u UnionSet) Find(i int) int {
if i == u.fa[i] {
return i
} else {
u.fa[i] = u.Find(u.fa[i])
return u.fa[i]
}
}
状态压缩
上述代码中,find函数中做了一个特殊的处理,那就是我在递归寻找他的代表节点时,会直接将他自身的节点与代表节点直接连接,这样下次调用find函数时,效率会大大提升。
为什么这样做
在进行合并操作时,我们是直接找到他们各自的代表节点,然后进行合并操作的。
这样的话,随着合并次数的增加,构建出树的深度也会随之增加,为了提高效率,我们需要在搜索代表节点时,将其自身节点与根节点建立直接联系,具体读者可以自行画图。
离线算法
离线算法也是之前没见过的算法,其实说是没见过,但是他的思想却很常见
在本题中,给到了一个queries数组,我们可以再声明一个ptr数组
ptr[i] 的含义是 我们要优先处理的第i个queries数组的下标。
一开始有些难以理解,画个图
在初始状态下,ptr中存放的都是指向对应queries数组的下标指针,在解题过程中,我们可能需要通过调整queries数组的顺序来达到一定的时间复杂度。但是queries数组的顺序却是定死的。这个时候我们可以通过调整ptr数组顺序,来间接实现我们的要求。由于ptr中存放的是queries的下标“指针”,因此我们可以很方便的通过ptr来访问到原数组。
这就是离线算法了。
解题
ok,有了这两个概念之后,本题迎刃而解了
题中所给的edgeList数组,我们根据他的disi进行从小到大的排序 当然queries数组也要给他根据limit进行离线排序 我们把每条距离小于limit的两个点合并到相同的集合当中去,超过limit的直接不管,这样一个集合当中,所有点都是互通的,并且我们只需要判断两个点是否属于一个集合就可以知道这条线路满不满足条件了。
为什么排序/离线排序
我们将queries数组根据limit离线排序后,优先处理小limit,这样就可以保证,小limit满足的边,我下一个大limit也可以满足,这样就减少了重复检索。大大提升了效率。
代码
class Solution {
public boolean[] distanceLimitedPathsExist(int n, int[][] edgeList, int[][] queries) {
Arrays.sort(edgeList,(a,b)->a[2]-b[2]);
Integer[] ptr = new Integer[queries.length];
for(int i = 0;i<queries.length;i++){
ptr[i] = i;
}
Arrays.sort(ptr,(a,b)->queries[a][2]-queries[b][2]);
int[] fa = new int[n];
int k = 0;
boolean[] ans = new boolean[queries.length];
for(int i = 0;i<n;i++){
fa[i] = i;
}
for(int i=0;i<ptr.length;i++){
int tmp = ptr[i];
while(k<edgeList.length&&edgeList[k][2]<queries[tmp][2]){
union(fa,edgeList[k][0],edgeList[k][1]);
k++;
}
ans[tmp] = find(fa,queries[tmp][0])==find(fa,queries[tmp][1]);
}
return ans;
}
public int find(int[] fa,int a){
if(fa[a]==a){
return a;
}else {
fa[a] = find(fa,fa[a]);
return fa[a];
}
}
public void union(int[] fa,int i,int j){
int x = find(fa,i);
int y = find(fa,j);
fa[x] = y;
}
}