Files
leetcode-master/problems/0797.所有可能的路径.md
Asterisk bd2832ec6a Update 0797.所有可能的路径.md
添加Python实现
2022-09-09 12:42:55 -04:00

11 KiB
Raw Blame History

看一下 算法4深搜是怎么讲的

797.所有可能的路径

本题是一道 原汁原味的 深度优先搜索dfs模板题那么用这道题目 来讲解 深搜最合适不过了。

接下来给大家详细讲解dfs

dfs 与 bfs 区别

先来了解dfs的过程很多录友可能对dfs深度优先搜索bfs广度优先搜索分不清。

先给大家说一下两者大概的区别:

  • dfs是可一个方向去搜不到黄河不回头直到遇到绝境了搜不下去了在换方向换方向的过程就涉及到了回溯
  • bfs是先把本节点所连接的所有节点遍历一遍走到下一个节点的时候再把连接节点的所有节点遍历一遍搜索方向更像是广度四面八方的搜索过程。

当然以上讲的是,大体可以这么理解,接下来 我们详细讲解dfsbfs在用单独一篇文章详细讲解

dfs 搜索过程

上面说道dfs是可一个方向搜不到黄河不回头。 那么我们来举一个例子。

如图一是一个无向图我们要搜索从节点1到节点6的所有路径。

图一

那么dfs搜索的第一条路径是这样的 假设第一次延默认方向就找到了节点6图二

图二

此时我们找到了节点6遇到黄河了是不是应该回头了那么应该再去搜索其他方向了。 如图三:

图三

路径2撤销了改变了方向走路径3红色线 接着也找到终点6。 那么撤销路径2改为路径3在dfs中其实就是回溯的过程这一点很重要很多录友都不理解dfs代码中回溯是用来干什么的

又找到了一条从节点1到节点6的路径又到黄河了此时再回头下图图四中路径4撤销回溯的过程改为路径5。

图四

又找到了一条从节点1到节点6的路径又到黄河了此时再回头下图图五路径6撤销回溯的过程改为路径7路径8 和 路径7路径9 结果发现死路一条,都走到了自己走过的节点。

图五

那么节点2所连接路径和节点3所链接的路径 都走过了撤销路径只能向上回退去选择撤销当初节点4的选择也就是撤销路径5改为路径10 。 如图图六:

图六

上图演示中,其实我并没有把 所有的 从节点1 到节点6的dfs深度优先搜索的过程都画出来那样太冗余了但 已经把dfs 关键的地方都涉及到了,关键就两点:

  • 搜索方向,是认准一个方向搜,直到碰壁之后在换方向
  • 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。

代码框架

正式因为dfs搜索可一个方向并需要回溯所以用递归的方式来实现是最方便的。

很多录友对回溯很陌生,建议先看看码随想录,回溯算法章节

有递归的地方就有回溯,那么回溯在哪里呢?

就地递归函数的下面,例如如下代码:

void dfs(参数) {
    处理节点
    dfs(图,选择的节点); // 递归
    回溯,撤销处理结果
}

可以看到回溯操作就在递归函数的下面,递归和回溯是相辅相成的。

在讲解二叉树章节的时候二叉树的递归法其实就是dfs而二叉树的迭代法就是bfs广度优先搜索

所以dfsbfs其实是基础搜索算法也广泛应用与其他数据结构与算法中

我们在回顾一下回溯法的代码框架:

void backtracking(参数) {
    if (终止条件) {
        存放结果;
        return;
    }
    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        处理节点;
        backtracking(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

回溯算法其实就是dfs的过程这里给出dfs的代码框架

void dfs(参数) {
    if (终止条件) {
        存放结果;
        return;
    }

    for (选择:本节点所连接的其他节点) {
        处理节点;
        dfs(图,选择的节点); // 递归
        回溯,撤销处理结果
    }
}

可以发现dfs的代码框架和回溯算法的代码框架是差不多的。

下面我在用 深搜三部曲,来解读 dfs的代码框架。

深搜三部曲

二叉树递归讲解中,给出了递归三部曲。

回溯算法讲解中,给出了 回溯三部曲。

其实深搜也是一样的,深搜三部曲如下:

  1. 确认递归函数,参数
void dfs(参数)

通常我们递归的时候,我们递归搜索需要了解哪些参数,其实也可以在写递归函数的时候,发现需要什么参数,再去补充就可以。

一般情况,深搜需要 二维数组数组结构保存所有路径,需要一维数组保存单一路径,这种保存结果的数组,我们可以定义一个全局遍历,避免让我们的函数参数过多。

例如这样:

vector<vector<int>> result; // 保存符合条件的所有路径
vector<int> path; // 起点到终点的路径
void dfs (图,目前搜索的节点)  

但这种写法看个人习惯,不强求。

  1. 确认终止条件

终止条件很重要很多同学写dfs的时候之所以容易死循环栈溢出等等这些问题都是因为终止条件没有想清楚。

if (终止条件) {
    存放结果;
    return;
}

终止添加不仅是结束本层递归,同时也是我们收获结果的时候。

另外其实很多dfs写法没有写终止条件其实终止条件写在了 下面dfs递归的逻辑里了也就是不符合条件直接不会向下递归。这里如果大家不理解的话没关系后面会有具体题目来讲解。

  • 841.钥匙和房间
    1. 岛屿数量
  1. 处理目前搜索节点出发的路径

一般这里就是一个for循环的操作去遍历 目前搜索节点 所能到的所有节点。

for (选择:本节点所连接的其他节点) {
    处理节点;
    dfs(图,选择的节点); // 递归
    回溯,撤销处理结果
}

不少录友疑惑的地方,都是 dfs代码框架中for循环里分明已经处理节点了那么 dfs函数下面 为什么还要撤销的呢。

如图七所示, 路径2 已经走到了 目的地节点6那么 路径2 是如何撤销,然后改为 路径3呢 其实这就是 回溯的过程撤销路径2走换下一个方向。

图七

总结

我们讲解了dfs 和 bfs的大体区别bfs详细过程下篇来讲dfs的搜索过程以及代码框架。

最后还有 深搜三部曲来解读这份代码框架。

以上如果大家都能理解了,其实搜索的代码就很好写,具体题目套用具体场景就可以了。

797. 所有可能的路径

思路

  1. 确认递归函数,参数

首先我们dfs函数一定要存一个图用来遍历的还要存一个目前我们遍历的节点定义为x

至于 单一路径,和路径集合可以放在全局变量,那么代码是这样的:

vector<vector<int>> result; // 收集符合条件的路径
vector<int> path; // 0节点到终点的路径
// x目前遍历的节点
// graph存当前的图
void dfs (vector<vector<int>>& graph, int x) 
  1. 确认终止条件

什么时候我们就找到一条路径了?

当目前遍历的节点 为 最后一个节点的时候,就找到了一条,从 出发点到终止点的路径。

当前遍历的节点我们定义为x最后一点节点就是 graph.size() - 1。

所以 但 x 等于 graph.size() - 1 的时候就找到一条有效路径。 代码如下:

// 要求从节点 0 到节点 n-1 的路径并输出,所以是 graph.size() - 1
if (x == graph.size() - 1) { // 找到符合条件的一条路径
    result.push_back(path); // 收集有效路径
    return;
}
  1. 处理目前搜索节点出发的路径

接下来是走 当前遍历节点x的下一个节点。

首先是要找到 x节点链接了哪些节点呢 遍历方式是这样的:

for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点

接下来就是将 选中的x所连接的节点加入到 单一路劲来。

path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来

当前遍历的节点就是 graph[x][i] 了,所以进入下一层递归

dfs(graph, graph[x][i]); // 进入下一层递归

最后就是回溯的过程,撤销本次添加节点的操作。 该过程整体代码:

for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点
    path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来
    dfs(graph, graph[x][i]); // 进入下一层递归
    path.pop_back(); // 回溯,撤销本节点
}

本题代码

class Solution {
private:
    vector<vector<int>> result; // 收集符合条件的路径
    vector<int> path; // 0节点到终点的路径
    // x目前遍历的节点
    // graph存当前的图
    void dfs (vector<vector<int>>& graph, int x) {
        // 要求从节点 0 到节点 n-1 的路径并输出,所以是 graph.size() - 1
        if (x == graph.size() - 1) { // 找到符合条件的一条路径
            result.push_back(path);
            return;
        }
        for (int i = 0; i < graph[x].size(); i++) { // 遍历节点n链接的所有节点
            path.push_back(graph[x][i]); // 遍历到的节点加入到路径中来
            dfs(graph, graph[x][i]); // 进入下一层递归
            path.pop_back(); // 回溯,撤销本节点
        }
    }
public:
    vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {
        path.push_back(0); // 无论什么路径已经是从0节点出发
        dfs(graph, 0); // 开始遍历
        return result;
    }
};

其他语言版本

Java

Python

class Solution:
    def __init__(self):
        self.result = []
        self.path = [0]

    def allPathsSourceTarget(self, graph: List[List[int]]) -> List[List[int]]:
        if not graph: return []

        self.dfs(graph, 0)
        return self.result
    
    def dfs(self, graph, root: int):
        if root == len(graph) - 1:  # 成功找到一条路径时
            # ***Python的list是mutable类型***
            # ***回溯中必须使用Deep Copy***
            self.result.append(self.path[:]) 
            return
        
        for node in graph[root]:   # 遍历节点n的所有后序节点
            self.path.append(node)
            self.dfs(graph, node)
            self.path.pop() # 回溯

Go