主要参考 零神 zerotrac 和 liuyubobobo 的题解——
“https://leetcode-cn.com/problems/checking-existence-of-edge-length-limited-paths/solution/jian-cha-bian-chang-du-xian-zhi-de-lu-ji-c756/”
“https://leetcode-cn.com/problems/checking-existence-of-edge-length-limited-paths/solution/jie-zhe-ge-wen-ti-ke-pu-yi-xia-shi-yao-j-pn1b/”
前提知识点——
知识点 1 :离线思维
「离线」的意思是,对于一道题目会给出若干询问,而这些询问是全部提前给出的,也就是说,你不必按照询问的顺序依次对它们进行处理,而是可以按照某种顺序(例如全序、偏序(拓扑序)、树的 DFS 序等)或者把所有询问看成一个整体(例如整体二分、莫队算法等)进行处理。
与「离线」相对应的是「在线」思维,即所有的询问是依次给出的,在返回第 k 个询问的答案之前,不会获得第 k+1个询问。
实际上,力扣平台上几乎所有的题目都是「离线」的,即一次性给出所有的询问。但在大部分情况下,我们按照下标顺序处理这些询问是没有问题的,也就是用「在线」的思维在「离线」的场景下解决问题。然而对于本题而言,我们必须按照一定的顺序处理 queries 中的询问,否则会使得时间复杂度没有保证。
举两个最简单的例子来说明——
以排序算法为例,插入排序算法是一种在线算法。因为可以把插入排序算法的待排序数组看做是一个数据流。插入排序算法顺次把每一个数据插入到当前排好序数组部分的正确位置。在排序过程中,即使后面源源不断来新的数据也不怕,整个算法照常进行。
选择排序算法则是一种离线算法。因为选择排序算法一上来要找到整个数组中最小的元素;然后找第二小元素;以此类推。这就要求不能再有新的数据了。因为刚找到最小元素,再来的新数据中有更小的元素,之前的计算就不正确了。
再举一个例子,对于 topK 问题(找前 k 小或者前 k 大的元素)
使用一个大小为 k 的优先队列是在线算法,虽然时间复杂度是 O(nlogk),但整个算法不需要一次性知道所有的数据,可以处理数据流;
使用快排的思想做改进,topK 问题可以在 O(n) 时间解决。但这是一个离线的算法。初始必须知道所有的数据,才能完成。
知识点2 :查并集
并查集(Union-find Sets)是一种非常精巧而实用的数据结构,它主要用于处理一些不相交集合的合并问题。一些常见的用途有求连通子图、求最小生成树的 Kruskal 算法和求最近公共祖先(Least Common Ancestors, LCA)等。每个集合可能包含一个或多个元素,并选出集合中的某个元素作为代表。每个集合中具体包含了哪些元素是不关心的,具体选择哪个元素作为代表一般也是不关心的。我们关心的是,对于给定的元素,可以很快的找到这个元素所在的集合(的代表),以及合并两个元素所在的集合,而且这些操作的时间复杂度都是常数级的。
并查集的基本操作有三个:
1. makeSet(s):建立一个新的并查集,其中包含 s 个单元素集合。
2. unionSet(x, y):把元素 x 和元素 y 所在的集合合并,要求 x 和 y 所在的集合不相交,如果相交则不合并。
这里合并在优化的时候 有两种方法:
(a) 按秩合并
即总是将更小的树连接至更大的树上。因为影响运行时间的是树的深度,更小的树添加到更深的树的根上将不会增加秩除非它们的秩相同。
在这个算法中,术语“秩”替代了“深度”,因为同时应用了路径压缩时(见下文)秩将不会与高度相同。
单元素的树的秩定义为0,当两棵秩同为r的树联合时,它们的秩r+1。只使用这个方法将使最坏的运行时间提高至每个MakeSet、Union或Find操作
(b) 路径压缩
一种在执行“查找”时扁平化树结构的方法。
关键在于在路径上的每个节点都可以直接连接到根上;他们都有同样的表示方法。为了达到这样的效果,
Find
递归地经过树,改变每一个节点的引用到根节点。得到的树将更加扁平,为以后直接或者间接引用节点的操作加速。
3. find(x):找到元素 x 所在的集合的代表,该操作也可以用于判断两个元素是否位于同一个集合,只要将它们各自的代表比较一下就可以了。
查并集的用途:
1、维护无向图的连通性。支持判断两个点是否在同一连通块内
2、判断增加一条边是否会产生环:用在求解最小生成树的Kruskal算法里。
题目描述——
看代码即可
class Solution {
public:
/*
因为queries的数组大小是10^5大小,所以对于每一个queries中的询问进行递归依次处理一定会超时
所以想到用方法将queries和edgelist进行排序,分别按照 limits 和 length
这样依次对queries中的询问进行访问时,将edgelist中的比该limits中的边加入到现有的图中,然后利用查并集来判断这次询问中的起点和终点是否属于一个连通图中(因为当前图中的边都是小于limit的,所以若联通就一定会有符合要求的路径存在) 也不需要对于0->1 和 1->0这样的边进行特殊处理。
该题中因为对queries 进行对 limit 排序后,本来的索引会发生变化,所以在每个queries中加入 index 的属性,这样排序过后对于每一个querie的结果记录到res[queries[3]]中
*/
// 查并集
vector<int> parent;
// parent[i] = j -> 表示 节点 i 的父亲是 j
vector<int> rank;
// rank[i] = k -> 表示 以节点 i 的树的深度是 k
int findParent(int a)
{
while(a != parent[a]) a = parent[a];
return a;
}
void union_ab(int a,int b)
{
while(a != parent[a]) a = parent[a];
while(b != parent[b]) b = parent[b];
if(a == b) return ;
if(rank[a] > rank[b])
parent[b] = a;
else
{
parent[a] = b;
if(rank[a] == rank[b]) rank[b]++;
}
}
bool if_connect(int a,int b)
{
while(a != parent[a]) a = parent[a];
while(b != parent[b]) b = parent[b];
return a == b;
}
vector<bool> distanceLimitedPathsExist(int n, vector<vector<int>>& edgeList, vector<vector<int>>& queries) {
parent.resize(n);
rank.resize(n);
for(int i=0;i<n;i++)
{
parent[i] = i;
rank[i] = 0;
}
for(int i =0;i<queries.size();i++)
queries[i].push_back(i);
vector<bool> res(queries.size(),false);
sort(queries.begin(),queries.end(),[](vector<int>& a,vector<int>& b){
return a[2]<b[2];
});
sort(edgeList.begin(),edgeList.end(),[](vector<int>& a,vector<int>& b){
return a[2] < b[2];
});
int f = 0;
for(int i =0;i<queries.size();i++)
{
int limit = queries[i][2];
while(f<edgeList.size() && edgeList[f][2] < limit)
{
union_ab(edgeList[f][0],edgeList[f][1]);
f++;
}
res[queries[i][3]] = if_connect(queries[i][0],queries[i][1]);
}
return res;
}
};