LeetCode 面试题 04.01. 节点间通路
题目
解题
解题一:邻接表+广度优先搜索
var findWhetherExistsPath = function(n, graph, start, target) {
// 创建 邻接表
let record = new Map();
for (let [src, des] of graph) {
if (record[src]) record[src].add(des);
else record[src] = new Set([des]);
}
let graphArr = [start];
let isVisited = new Set();
while (graphArr.length > 0) {
let front = graphArr.shift();
if (front === target) return true; // 找到,返回 true
if (isVisited.has(front) === true) continue; // 访问过,跳过
isVisited.add(front); // front 变成 visited
// 如果有从 front 出发的路径,将 next 元素加入 graphArr
if (record[front]) {
for (let next of record[front]) {
graphArr.push(next);
}
}
}
return false;
};
另一种写法是在加入 graphArr 的时候进行判断(判断更早,graphArr 里没有重复元素),但语义没有 isVisited 那么明显。
// javascript
var findWhetherExistsPath = function(n, graph, start, target) {
if (start === target) return true; // 必须有
// 创建 邻接表
let record = new Map();
for (let [src, des] of graph) {
if (record[src]) record[src].add(des);
else record[src] = new Set([des]);
}
let graphArr = [start];
let graphSet = new Set([start]); // 加入 start 元素
while (graphArr.length > 0) {
let front = graphArr.shift();
if (record[front]) { // 如果有从 front 出发的路径
for (let next of record[front]) {
if (next === target) return true; // 判断是不是 target
// 如果已经加入则不重复加,即 graphArr 无重复元素
if (graphSet.has(next) === false) {
graphArr.push(next);
graphSet.add(next);
}
}
}
}
return false;
};
isVisited 和 graphSet 都是为了应对自环,平行边用 set 存储邻接表时就过滤掉了,对结果无影响。
如果不创建邻接表,每次都去 graph 里找 front 指向的 next,时间复杂度为 O ( n e ) O(ne) O(ne),会超出时间限制。使用邻接表,时间复杂度为 O ( n + e ) O(n+e) O(n+e),空间复杂度为 O ( n + e ) O(n+e) O(n+e)。
解题二:邻接表+深度优先搜索
// javascript
var findWhetherExistsPath = function(n, graph, start, target) {
let isVisited = new Set();
// 创建 邻接表
let record = new Map();
for (let [src, des] of graph) {
if (record[src]) record[src].add(des);
else record[src] = new Set([des]);
}
return canFindExistingPath(record, start, target, isVisited);
};
var canFindExistingPath = function(graph, start, target, isVisited) {
if (start === target) return true;
if (isVisited.has(start) === true) return false;
isVisited.add(start);
if (graph[start]) {
for (let des of graph[start]) {
if (canFindExistingPath(graph, des, target, isVisited) === true) {
return true;
}
}
}
return false;
};
时间复杂度为 O ( n + e ) O(n+e) O(n+e),空间复杂度为 O ( n + e ) O(n+e) O(n+e)。
解法三:Bellman Ford
// javascript
var findWhetherExistsPath = function(n, graph, start, target) {
if (start === target) return true;
let reachable = new Array(n).fill(false);
reachable[start] = true;
for (let i = 0; i < n; i++) {
for (let [u, v] of graph) {
if (reachable[u] === true) {
reachable[v] = true;
}
}
}
return reachable[target];
};
上面是 Bellman Ford 算法,时间复杂度为 O ( n e ) O(ne) O(ne),空间复杂度为 O ( n ) O(n) O(n),会超过时间限制。下面的改写比较讨巧,因为题目给的测试用例是排序的,因而可以只遍历一遍 graph,将时间复杂度降为 O ( e ) O(e) O(e)。
// javascript
var findWhetherExistsPath = function(n, graph, start, target) {
if (start === target) return true;
let reachable = new Array(n).fill(false);
reachable[start] = true;
for (let [u, v] of graph) {
if (reachable[u] === true) {
if (v === target) return true;
reachable[v] = true;
}
}
return false;
};
可求解成功:n=3, graph=[[0, 1], [1, 2]], start=0, target=2
会求解失败:n=3, graph=[[1, 2], [0, 1]], start=0, target=2
为了解决第二种失败情况,可以像下面一样进行改写:
// javascript
var findWhetherExistsPath = function(n, graph, start, target) {
if (start === target) return true;
let set = new Set([start]);
let count = 1;
while (true) {
for (let [u, v] of graph) {
if (set.has(u) === true) {
if (v === target) return true;
set.add(v);
}
}
// 如果 set 里未新增元素,则 start 能到的元素全部找出,break
// 如果 set 里新增元素,则 还能找到其他 start 能到达但未被检查过的路径
if (set.size === count) break;
count = set.size;
}
return false;
};
解法四:倒序递归+深度优先搜索
// javascript
var findWhetherExistsPath(n, graph, start, target) {
// 创建访问状态数组
let isVisited = new Array(n).fill(false);
// DFS
return helper(graph, start, target,isVisited);
};
var helper(graph, start, target, isVisited) {
// 深度优先搜索
for (let i = 0; i < graph.length; i++) {
// 确保当前路径未被访问(该判断主要是为了防止图中自环出现死循环的情况)
if (!isVisited[i]) {
// 若当前路径起点与终点相符,则直接返回结果
if (graph[i][0] === start && graph[i][1] === target) {
return true;
}
// 设置访问标志
isVisited[i] = true;
// DFS 关键代码,思路:同时逐渐压缩搜索区间
if (graph[i][1] === target && helper(graph, start, graph[i][0], isVisited)) {
return true;
}
// 清除访问标志
isVisited[i] = false;
}
}
return false;
}
作者解释见下图,隐约能意会是这个道理,但想得不是特别明白,总感觉解法四能通过而正向递归不能跟 LeetCode 最后一个奇长无比的测试用例有关:如果进行修改,让很多边指向 target 的话,helper 调用次数增加,解法四也会超时。
总结一下,解法一和二为正统解法;解法三和四有点奇法妙招的意味,多少讨了些巧(测试用例正好能通过),启发是碰到问题如果能找到一些特性从而简化问题,也是能出奇制胜滴。