图解 BFS 路径搜索:LeetCode1971
图解 BFS 路径搜索:LeetCode
-
题目描述:有一个具有 n 个顶点的 双向 图,其中每个顶点标记从 0 到 n - 1(包含 0 和 n - 1)。图中的边用一个二维整数数组 edges 表示,其中 edges[i] = [ui, vi] 表示顶点 ui 和顶点 vi 之间的双向边。 每个顶点对由 最多一条 边连接,并且没有顶点存在与自身相连的边。请你确定是否存在从顶点 source 开始,到顶点 destination 结束的 有效路径 。
-
给你数组 edges 和整数 n、source 和 destination,如果从 source 到 destination 存在 有效路径 ,则返回 true,否则返回 false 。
在图论算法中,判断节点连通性与收集所有路径是经典场景。本文围绕无向图,结合具体代码,深入拆解判断两节点是否连通(validPath
) 与 收集两节点间所有路径(allPaths
) 的实现逻辑,以输入 n = 3, edges = [[0,1],[1,2],[2,0]], source = 0, destination = 2
为例,清晰展现广度优先搜索(BFS)的运作流程。
一、基础准备:图的邻接表构建
无论判断连通还是收集路径,第一步需将图的边结构转换为邻接表,方便后续遍历。代码中通过 create_tree_node
方法实现:
void create_tree_node(int n, vector<vector<int>>& edges) {adj.resize(n); for (const auto& edge : edges) {int u = edge[0], v = edge[1];adj[u].push_back(v); adj[v].push_back(u); }
}
逻辑拆解(以输入为例):
- 初始化:
adj.resize(3)
为节点0
、1
、2
各分配一个vector<int>
存储邻居。 - 遍历边:每条边双向存邻接表,如边
[0,1]
会让adj[0]
加入1
,adj[1]
加入0
。最终邻接表状态:adj[0] = [1, 2]; adj[1] = [0, 2]; adj[2] = [1, 0];
二、判断两节点是否连通(validPath
逻辑)
目标:检查从 source
到 destination
是否存在至少一条路径,用 BFS 实现“层序遍历 + 访问标记”。
核心代码回顾
bool validPath(int n, vector<vector<int>>& edges, int source, int destination) {create_tree_node(n, edges); vector<bool> visited(n, false); queue<int> q; q.push(source); visited[source] = true; while (!q.empty()) {int current = q.front(); q.pop(); if (current == destination) return true; for (int neighbor : adj[current]) { if (!visited[neighbor]) {visited[neighbor] = true; q.push(neighbor); }}}return false;
}
流程拆解(输入 source=0, destination=2
)
- 初始化:
visited
标记数组全为false
;队列q
放入0
,并标记visited[0] = true
。 - 第一次循环:
- 取出
current = 0
,检查是否是2
→ 否。 - 遍历
adj[0]
邻居[1, 2]
:1
未访问 → 标记visited[1]=true
,入队q = [1]
。2
未访问 → 标记visited[2]=true
,入队q = [1, 2]
。
- 取出
- 第二次循环:
- 取出
current = 1
,检查是否是2
→ 否。 - 遍历
adj[1]
邻居[0, 2]
:0
已访问 → 跳过;2
已访问 → 跳过。
- 取出
- 第三次循环:
- 取出
current = 2
,检查是否是2
→ 是!返回true
,流程结束。
- 取出
三、收集两节点间所有路径(allPaths
逻辑)
目标:找到从 source
到 destination
的全部路径,需用“路径驱动的 BFS”,队列存储完整路径。
核心代码回顾
vector<vector<int>> allPaths(int n, vector<vector<int>>& edges, int source, int destination) {create_tree_node(n, edges); vector<vector<int>> result; queue<vector<int>> q; q.push({source}); while (!q.empty()) {vector<int> path = q.front(); q.pop(); int current = path.back(); if (current == destination) {result.push_back(path); continue; }for (int neighbor : adj[current]) { bool is_cycle = false; for (int node : path) { if (node == neighbor) { is_cycle = true; break; }}if (!is_cycle) { vector<int> newPath = path; newPath.push_back(neighbor); q.push(newPath); }}}return result;
}
流程拆解(输入 source=0, destination=2
)
- 初始化:结果集
result
为空;队列q
放入初始路径{0}
。 - 第一次循环:
- 取出路径
path = {0}
,current = 0
(路径末尾节点)。 - 检查是否是
2
→ 否。 - 遍历
adj[0]
邻居[1, 2]
:- 邻居
1
:路径{0}
中无1
→ 构造新路径{0,1}
,入队q = [ {0,1} ]
。 - 邻居
2
:路径{0}
中无2
→ 构造新路径{0,2}
,入队q = [ {0,1}, {0,2} ]
。
- 邻居
- 取出路径
- 第二次循环:
- 取出路径
path = {0,1}
,current = 1
。 - 检查是否是
2
→ 否。 - 遍历
adj[1]
邻居[0, 2]
:- 邻居
0
:路径{0,1}
中已有0
→ 跳过(防环)。 - 邻居
2
:路径{0,1}
中无2
→ 构造新路径{0,1,2}
,入队q = [ {0,2}, {0,1,2} ]
。
- 邻居
- 取出路径
- 第三次循环:
- 取出路径
path = {0,2}
,current = 2
→ 是目标!加入result
,result = [ {0,2} ]
。
- 取出路径
- 第四次循环:
- 取出路径
path = {0,1,2}
,current = 2
→ 是目标!加入result
,result = [ {0,2}, {0,1,2} ]
。
- 取出路径
四、关键逻辑对比与总结
场景 | 核心思路 | 队列存储内容 | 终止条件 | 适用场景 |
---|---|---|---|---|
判断连通(validPath ) | 节点驱动 BFS,标记访问状态 | 单个节点 | 找到目标节点立即返回 | 只需判断是否连通 |
收集所有路径(allPaths ) | 路径驱动 BFS,防环扩展路径 | 完整路径 | 收集所有到达目标的路径 | 需枚举全部路径场景 |
两种实现均基于 BFS 思想,但因目标不同,队列存储与逻辑分支有明显差异。邻接表的双向边处理(无向图特性)、访问标记/防环逻辑,是确保算法正确的关键。
通过本文拆解,可清晰理解 BFS 在图论问题中的灵活应用——从简单连通性判断,到复杂路径收集,核心逻辑的扩展与适配思路值得反复揣摩。