题目描述
有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。
请你确定是否存在从顶点 source 开始,到顶点 destination 结束的 有效路径 。
给你数组 edges 和整数 n、source 和 destination,如果从 source 到 destination 存在 有效路径 ,则返回 true,否则返回 false 。示例 1:
输入:n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
输出:true
解释:存在由顶点 0 到顶点 2 的路径:示例 2:
输入:n = 6, edges = [[0,1],[0,2],[3,5],[5,4],[4,3]], source = 0, destination = 5
输出:false
解释:不存在由顶点 0 到顶点 5 的路径.提示:
- 1 <= n <= 2 * 105
- 0 <= edges.length <= 2 * 105
- edges[i].length == 2
- 0 <= ui, vi <= n - 1
- ui != vi
- 0 <= source, destination <= n - 1
- 不存在重复边
- 不存在指向顶点自身的边
方法一:并查集
- 思路:
- 使用 并查集,依次遍历给定的 edges 数组 , 将存在双向边的两个点合并,最后在并查集中查询 source 和 destination 是否连通即可,如果他们不在一个集合,说明不连通。
- 情况
- 通过;
- 收获
- 我很快就想到了要用并查集的思想,这是我做题以来的进步,但还是有缺点,比如并查集模板我没有记下来,我是看着之前的题解又敲了一遍,还是要多做题。
- 时间复杂度:O(n + m * α(m)),n 是图中的顶点数,m 是图中边的数目, α 是反阿克曼函数。 并查集的初始化需要O(n)的时间,然后遍历 m 条边并执行 m 次合并操作,最后对 source 和 destination 进行一次查询操作。查询与合并的单次操作时间复杂度是O(α(m)),因此合并和查询的时间复杂度为 O(m * α(m)),总的时间复杂度为 O(n + m * α(m))。
空间复杂度:O(n),n 为节点数量;
class UF{
public:
vector<int> fa; // 存储每个节点的父节点
vector<int> sz; // 只有节点是祖宗节点的时候才有意义,表示祖宗节点所在集合的节点数
int n; // 节点数量
int comp_cnt;
public:
// 有参数的构造函数
UF(int n_): n(n_), comp_cnt(n_), fa(n_), sz(n_, 1){
// iota 自增函数
iota(fa.begin(), fa.end(), 0);
}
// 寻找元素x的集合的祖宗节点
int findset(int x){
return fa[x] == x ? x : fa[x] = findset(fa[x]);
}
// 合并
bool unite(int x, int y){
// 寻找各自的祖宗节点
x = findset(x);
y = findset(y);
if(x == y) return false; // 无需合并
// 合并
// 确保合并到元素较多的集合中
if(sz[x] < sz[y]) swap(x, y);
fa[y] = x;
sz[x] += sz[y];
-- comp_cnt;
return true;
}
// 判断x和y是否在同一集合里
bool connected(int x, int y){
x = findset(x);
y = findset(y);
return x == y;
}
};
class Solution {
public:
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
UF uf(n);
for(vector<int>& e : edges){
uf.unite(e[0], e[1]);
}
return uf.connected(source, destination);
}
};
方法二:DFS
- 思路:
- 先将 edges 转换成图 g , 然后使用 DFS ,判断是否存在从 source 到 destination 的路径。
- 数组 visit 记录已经访问过的顶点,避免重复访问。
- 首先从顶点 source 开始遍历并进行递归搜索。搜索时每次访问一个顶点 ,如果顶点等于 destination 则直接返回,否则将该顶点设置为已访问,并递归访问与该顶点相邻且未访问的顶点next ,如果通过 next 的路径可以访问到 destination ,此时直接返回 true , 当访问完所有的邻接节点仍然没有访问到 destination ,此时返回 false。
- 情况
- 通过;
- 收获
- 这道题的关键点是,对于 DFS 函数中,visit数组必须以引用的方式传入 ,否则会超时。
引用的一个重要作用就是作为函数的参数,如果有大的数据作为参数传递的时候,往往采取指针传递,因为这样可以避免较多的数据压栈,可以提高程序效率。
- 时间复杂度:O(n + m)。其中 n 是图中顶点数目, m 表示图中边的数目。对于图中的每个顶点或者每条边,我们最多只需要访问一次,因此时间复杂度为O(n + m) 。
空间复杂度:O(n + m),其中 n 是图中顶点数目, m 表示图中边的数目。空间复杂度取决于邻接顶点列表、记录每个顶点访问状态的数组和递归调用栈, 邻接顶点列表需要O(m + n)的存储空间,记录每个顶点访问状态的数组和递归调用栈分别需要 O(n)的空间,因此总的空间复杂度为 O(m + n)。
class Solution {
public:
bool DFS(int source, int destination, vector<vector<int>>& g, vector<bool> &visit){
if(source == destination) return true;
visit[source] = true;
for(int next : g[source]){
if(!visit[next] && DFS(next, destination, g, visit)){
return true;
}
}
return false;
}
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
vector<vector<int>> g(n); // 图数组
// 将 edges 转为 图
for(auto& e : edges){
int a = e[0], b = e[1];
g[a].emplace_back(b);
g[b].emplace_back(a);
}
vector<bool> visit(n, false); // 数组是否被访问过
return DFS(source, destination, g, visit);
}
};
方法三:BFS
- 思路:
- 先将 edges 转换成图 g , 然后使用 BFS ,判断是否存在从 source 到 destination 的路径。
- 数组 visit 记录已经访问过的顶点,避免重复访问。
- 遍历过程我们使用队列存储最近访问过的顶点,同时记录每个顶点的访问状态,每次从队列中取出顶点 vertex 时,将其未访问过的邻接顶点入队列。
- 初始时将顶点 source 设为已访问,,并将其入队列。每次将队列中的节点 vertex 出队,并将与 vertex 相邻且未访问的顶点 next 入队,并将 next 设为已访问。当队列为空或访问到顶点 destination 时遍历结束 ,返回顶点 destination 的访问状态即可。
- 情况
- 通过;
- 收获
- 通过这道题复习了 BFS,广度优先搜索;
- 时间复杂度:O(n + m)。其中 n 是图中顶点数目, m 表示图中边的数目。对于图中的每个顶点或者每条边,我们最多只需要访问一次,因此时间复杂度为O(n + m) 。
空间复杂度:O(n + m),其中 n 是图中顶点数目, m 表示图中边的数目。空间复杂度取决于邻接顶点列表、记录每个顶点访问状态的数组和队列, 邻接顶点列表需要O(m + n)的存储空间,记录每个顶点访问状态的数组需要 O(n)的空间,进行广度搜索时队列最多只有 n 个元素,因此总的空间复杂度为 O(m + n)。
class Solution {
public:
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {
vector<vector<int>> g(n); // 图数组
// 将 edges 转为 图
for(auto& e : edges){
int a = e[0], b = e[1];
g[a].emplace_back(b);
g[b].emplace_back(a);
}
vector<bool> visit(n, false); // 数组是否被访问过
queue<int> q;
q.emplace(source);
visit[source] = true;
while(!q.empty()){ // 当队列不为空时
int vertex = q.front();
q.pop();
if(vertex == destination) break;
for(int next : g[vertex]){
if(!visit[next]){ // 如果该点未被访问
q.emplace(next);
visit[next] = true;
}
}
}
return visit[destination];
}
};
参考文献:
- DFS算法原理及其具体流程