Day62|图part1:深度优先搜索理论基础、797. 所有可能的路径

深度优先搜索(DFS)理论基础

图的实质和存储方式

图实际上就是一棵多叉树,可以用以下的数据结构进行表示:

class Vertex {
    int id;
    vector<Vertex*> neighbors;
};

多叉树的:

/* 基本的 N 叉树节点 */
class TreeNode {
public:
    int val;
    vector<TreeNode*> children;
};

但实际上很少用这个实现图,一般用邻接表或邻接矩阵

image

image

邻接表很直观,我把每个节点 x 的邻居都存到一个列表里,然后把 x 和这个列表关联起来,这样就可以通过一个节点 x 找到它的所有相邻节点。

邻接矩阵则是一个二维布尔数组,我们权且称为 matrix,如果节点 xy 是相连的,那么就把 matrix[x][y] 设为 true(上图中绿色的方格代表 true)。如果想找节点 x 的邻居,去扫一圈 matrix[x][..] 就行了。

如果用代码的形式来表现,邻接表和邻接矩阵大概长这样:

// 邻接表
// graph[x] 存储 x 的所有邻居节点
vector<int> graph[];

// 邻接矩阵
// matrix[x][y] 记录 x 是否有一条指向 y 的边
bool matrix[][];

对于邻接表,好处是占用的空间少。

你看邻接矩阵里面空着那么多位置,肯定需要更多的存储空间。

但是,邻接表无法快速判断两个节点是否相邻。

比如说我想判断节点 1 是否和节点 3 相邻,我要去邻接表里 1 对应的邻居列表里查找 3 是否存在。但对于邻接矩阵就简单了,只要看看 matrix[1][3] 就知道了,效率高。

图的遍历

图的遍历就是多叉树的遍历,经历回溯的过程,首先看下多叉树的遍历框架:

/* 多叉树遍历框架 */
void traverse(TreeNode* root) {
    if (root == nullptr) return;
    // 前序位置
    for (TreeNode* child : root->children) {
        traverse(child);
    }
    // 后序位置
}

图和多叉树最大的区别是,图是可能包含环的,你从图的某一个节点开始遍历,有可能走了一圈又回到这个节点,而树不会出现这种情况,从某个节点出发必然走到叶子节点,绝不可能回到它自身。

所以,如果图包含环,遍历框架就要一个 visited 数组进行辅助:

// 记录被遍历过的节点
vector<bool> visited;
// 记录从起点到当前节点的路径
vector<bool> onPath;

/* 图遍历框架 */
void traverse(Graph graph, int s) {
    if (visited[s]) return;
    // 经过节点 s,标记为已遍历
    visited[s] = true;
    // 做选择:标记节点 s 在路径上
    onPath[s] = true;
    for (int neighbor : graph.neighbors(s)) {
        traverse(graph, neighbor);
    }
    // 撤销选择:节点 s 离开路径
    onPath[s] = false;
}

visited和onPath的区别:类比贪吃蛇游戏,visited 记录蛇经过过的格子,而 onPath 仅仅记录蛇身。在图的遍历过程中,onPath 用于判断是否成环,类比当贪吃蛇自己咬到自己(成环)的场景。

看到这个算法其实很像回溯算法的过程,不同的是回溯算法关注的不是节点,而是树枝。他们的区别可以反映在代码上:

// DFS 算法,关注点在节点
void traverse(TreeNode* root) {
    if (root == nullptr) return;
    printf("进入节点 %s", root);
    for (TreeNode* child : root->children) {
        traverse(child);
    }
    printf("离开节点 %s", root);
}

// 回溯算法,关注点在树枝
void backtrack(TreeNode *root) {
    if (root == nullptr) return;
    for (TreeNode* child : root->children) {
        // 做选择
        printf("从 %s 到 %s", root, child);
        backtrack(child);
        // 撤销选择
        printf("从 %s 到 %s", child, root);
    }
}

执行上面第一个的代码,会发现起始点被漏掉了。

因此对于图的遍历应该使用DFS算法,即把 onPath 的操作放到 for 循环外面,否则会漏掉记录起始点的遍历。

即把 onPath 的操作放到 for 循环外面,否则会漏掉记录起始点的遍历。

visited 数组,其目的很明显了,由于图可能含有环,visited 数组就是防止递归重复遍历同一个节点进入死循环的。

当然,如果题目告诉你图中不含环,可以把 visited 数组都省掉,基本就是多叉树的遍历。

深搜三部曲

与回溯三部曲一样,DFS也有一个框架;

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

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

  1. 确认递归函数,参数

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

  1. 确认终止条件

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

另外,其实很多dfs写法,没有写终止条件,其实终止条件写在了, 下面dfs递归的逻辑里了,也就是不符合条件,直接不会向下递归

  1. 处理目前搜索节点出发的路径

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

797. 所有可能的路径

leetcode链接:力扣题目链接

给你一个有 n 个节点的 有向无环图(DAG),
请你找出所有从节点 0 到节点 n-1 的路径并输出(不要求按特定顺序)

graph[i] 是一个从节点 i 可以访问的所有节点的列表(
即从节点 i 到节点 graph[i][j]存在一条有向边)。

示例1:

输入:graph = [[1,2],[3],[3],[]] 
输出:[[0,1,3],[0,2,3]] 
解释:有两条路径 0 -> 1 -> 30 -> 2 -> 3

image

示例II:
输入:graph = [[4,3,1],[3,2,4],[3],[4],[]] 
输出:[[0,4],[0,3,4],[0,1,3,4],[0,1,2,3,4],[0,1,4]]
  

image

这里注意到题目要求的是有向无环图,因此可以不用visited数组记录是否重复了。

这里的输入其实就是邻接表,

graph = [[1,2],[3],[3],[]]表示0节点和1,2相连,1节点和3相连等等。

深搜三部曲

  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(因为题目描述是找出所有从节点 0 到节点 n-1 的路径并输出)。

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

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

遍历与x相连的所有节点,查找其邻接表:

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

进入循环后,加入节点,dfs,回溯:

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<int> path;
    vector<vector<int>> res;
    void dfs(vector<vector<int>>& graph, int x){
        if(x == graph.size() - 1){
            res.push_back(path);
            return;
        }
        for(int i = 0; i < graph[x].size(); i++){
            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);
        dfs(graph,0);
        return res;
    }
};

总结

  • DFS与回溯算法相似, 也有深搜三部曲;
  • 对于有环图,使用visited数组标注是否遍历过;
  • 注意图的存储方式(邻接表常用)和遍历方式
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值