回溯算法基础,图的遍历算法基础

目录

1. 基本概念

1. 回溯算法的本质

2. 回溯算法与动态规划算法的区别

3. 回溯法和分支限界法的区别

4. 回溯算法的基本框架

2. 回溯算法题型

1. 子集型

1. 力扣78

2. 力扣90

2. 组合型

1. 力扣77

2. 力扣40

3. 力扣39

3. 排列型

1. 力扣46 全排列

2. 力扣47

3.  补充题

力扣51 n皇后

力扣52

3. DFS、BFS算法

1. 走迷宫:基于递归的DFS伪代码

2. 走迷宫:基于队列的BFS伪代码(类似于二叉树的层序遍历)

3. 图的遍历伪码

DFS

BFS

力扣797

2. 例题

1. 迷宫的判定

DFS

BFS

2. 迷宫之路径(DFS)

3. 迷宫之最短路径(BFS)

4. 力扣200 岛屿数量

DFS

BFS


1. 基本概念

1. 回溯算法的本质

在说回溯算法之前,先要提一下递归,因为递归能够实现 多重for循环 的功能,也是回溯算法的实现基础。

回溯算法尝试分多步解决一个问题。在分步解决问题的过程中,当它通过尝试发现现有的分步答案不能得到有效的正确的解答的时候,它将取消上一步甚至是上几步的计算(撤销,回溯),再通过其它的可能的分步解答再次尝试寻找问题的答案;当寻找到一个可行解后,也会回溯,继续寻找所有的可行解

回溯算法,即通过DFS的方式枚举问题的所有解,然后得到问题的所有可行解(其中当然包括最优解)。

回溯法解决的问题都能画出一颗状态空间树(多叉树),我们需要做的就是递归遍历这颗树(分步枚举所有可能的决策),收集所有的可行解

  • 状态空间树:每一步所选择的决策,构成多叉树的边(路径),汇集在一起,形成一颗多叉树
  • 递归什么?     递归遍历 状态空间树,寻找所有的可行解
  • 可行解是什么?  可行解是 状态空间树根节点到某个子节点间的路径,对应的是每步所作的决策【这个根据不同题目会有区别,是到某个子节点的路径还是到叶子节点的路径由题意定。】

2. 回溯算法与动态规划算法的区别

回溯算法是分步的去解决一个问题,是一个通过递归枚举问题所有解的过程,它遍历问题的所有解,然后找出问题的所有可行解(当然可行解中包含最优解)。一般的使用场景是求所有解的问题(比如全排列)

动态规划则是分解问题,通过子问题的答案(最优子结构)推出 全局最优解。一般的使用场景是求最值(参见上一篇文章中 最长公共子序列,最长重复子数组等等)

3. 回溯法和分支限界法的区别

遍历状态空间树的策略不同:回溯法采用深度优先搜索的方式遍历状态空间树求问题的所有解,分支限界法采用广度优先搜索的方式遍历状态空间树求问题的最优解。

设解决一个问题有 x = (x1, x2 ...xn)个步骤:

回溯法的每一步决策 是从 x中选择一个步骤xi来执行,状态空间树生成这种情况(添加树枝,路径对应着所做决策),然后重复操作(DFS)

分支分支限界法的每一步决策 是从 x中选择一个步骤xi,xi有执行或不执行两种情况,状态空间树生成这两种情况(添加树枝,路径对应着所做决策),然后重复操作(BFS)

4. 回溯算法的基本框架

回溯算法是一种递归枚举算法,而且能用回溯算法解决的题目都能抽象为一颗 状态空间树(多叉树)。所以回溯算法就是采用递归遍历这颗多叉树。所以从这个角度讲,回溯算法跟 DFS(深度优先搜索)很相似,都是 不撞南墙不回头 这种

DFS代码模板:

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

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

回溯算法代码模板:

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

    for (选择:本层集合中元素(树中节点孩子的数量就是集合的大小)) {
        做出选择
        backtrack(路径,选择列表); // 递归
        回溯,撤销处理结果
    }
}

通过以上分析,写回溯算法即写递归代码类似,我们要确定递归函数的形参,以及递归函数的边界条件(递归终点)。遍历整颗多叉树,找到所有的可行解

2. 回溯算法题型

回溯算法即书写递归函数,而递归函数的形参应该填什么,递归函数的终点如何确定应该是我们考虑的重点问题

  • 子集型、组合型:start变量防止出现重复元素
  • 排列型:used数组防止重复使用元素
  • 元素重复 但不能重复选择的情况,需要进行排序和剪枝,目的是为了去重
  • 元素不重复,但可以重复选择的情况,需要改递归函数参数(表现为状态空间树的形状不同)

1. 子集型

不在乎顺序,在乎元素,1,2,3 和 1,3,2 是两种相同的情况。

在空间树中体现为一颗左右子树高度不相同的一颗多叉树。递归函数中需使用一个额外的形参start,防止重复选择,例如第1题中,如果第一步决策选择了2,则第二步决策只能选择3,不能选择1,start就是控制这个的。

1. 力扣78

该题特点:元素不重复,不可重复选择(只有一个)。该题就是遍历这颗多叉树,到多叉树每一个节点的路径都是问题的解

class Solution {
public: 
    vector<vector<int>> res;  // 记录所有的可行解
    vector<int> path;   // 记录回溯算法的递归路径
    // 回溯算法核心函数,遍历子集问题的状态空间树
    void backtrack(vector<int>& nums, int start){
        res.push_back(path);
         if (start >= nums.size()) { // 终止条件可以不加
            return;
        } // 到叶子节点,递归结束
        // 回溯算法标准框架
        for(int i = start; i < nums.size(); i++){
            path.push_back(nums[i]);  // 处理节点
            backtrack(nums, i+1);   // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            path.pop_back();    // 撤销选择,回溯
        }
    }
    vector<vector<int>> subsets(vector<int>& nums) {
        backtrack(nums, 0);
        return res;
    }
};

2. 力扣90

该题特点:元素重复,不可重复选择(只有一个)。【去重:排序 + 剪枝】

画出空间树后,会发现有些结果重复出现,所以必须进行剪枝【如果一个节点有多条值相同的树枝相邻,则只遍历第一条,剩下的都剪掉,不要去遍历】,体现在代码上,需要先进行排序,让相同的元素靠在一起,如果发现 nums[i] == nums[i-1],则跳过

class Solution {
public:
    vector<vector<int>> res;  // 记录所有的可行解
    vector<int> path;  // 记录回溯算法的递归路径
    // 回溯算法核心函数,遍历子集问题的状态空间树
    void backtrack(vector<int>& nums, int start){
        res.push_back(path);
        if(start >= nums.size()) return;  // 到叶子节点,递归结束
        // 回溯算法标准框架
        for(int i = start; i <nums.size(); i++){
            //在同一层遍历中,如果后面的元素和前面的相同,只用输出一次前面的即可
            if(i > start && nums[i] == nums[i -1])  // 剪枝逻辑,值相同的相邻树枝,只遍历第一次出现的
                continue;
            path.push_back(nums[i]);
            backtrack(nums, i +1);
            path.pop_back();
        }
    }
    vector<vector<int>> subsetsWithDup(vector<int>& nums) {
        sort(nums.begin(), nums.end());  // 排序,让相同的元素靠在一起
        backtrack(nums, 0);
        return res;
    }
};

2. 组合型

在乎元素,同子集型问题很相似,相当于同一类问题。在空间树中体现为一颗左右子树高度不相同的一颗多叉树。递归函数中需使用一个额外的形参start,防止重复选择。例如第1题中,如果第一步决策选择了2,则第二步决策只能选择3,不能选择1,start就是控制这个的。

1. 力扣77

该题特点:元素不重复,不可重复选择。【与子集类问题第1题类似】

本题求解的是到状态空间树某一层节点的路径。比如 combine(3, 2) 的返回值应该是 [[1, 2], [1, 3], [2, 3]]

对应的状态空间树如图

class Solution {
public:
    vector<vector<int>> res;  // 记录所有的可行解
    vector<int> path;   // 记录回溯算法的递归路径
    // 回溯算法核心函数,遍历子集问题的状态空间树
    void backtrack(int n, int k, int start){
        if(path.size() == k){  // 递归终点:遍历到状态空间树的某层
            res.push_back(path); 
            return;
        }
        // 回溯算法标准框架
        for(int i = start; i <= n; i++){
            path.push_back(i);  // 处理节点
            backtrack(n, k, i+1);  //  // 通过 start 参数控制树枝的遍历,避免产生重复的子集
            path.pop_back();  // 回溯
        }
    }
    vector<vector<int>> combine(int n, int k) {
        backtrack(n, k, 1);
        return res;
    }
};

2. 力扣40

该题特点:元素可重复,不可重复选择。【与子集类问题第2题类似:需要去重,先排序后剪枝】

由于该题找的是candidates 中所有可以使数字和为 target 的组合,所以需要一个变量 sum 来统计路径上的和

class Solution {
public:
    vector<vector<int>> res;  // 记录所有可能的解
    vector<int> path;  // 记录一个可行解
    int sum;  // 记录路径和,方便我们判断递归边界
    // 回溯算法主函数
    void backtrack(vector<int>& candidates, int target, int start){
        // 终止条件1:找到能凑成target的组合时,没必要再往下找,再往下一定比 target大
        if(sum == target){
            res.push_back(path);
            return; 
        }
        // 终止条件2:当前组合值比 target大,没必要再往下找,再往下一定比 target大
        if(sum > target) return;

        // 回溯算法标准框架
        for(int i = start; i < candidates.size(); i++){
            // 剪枝操作,因为在此步骤前进行了排序,所以相同元素靠在一起,即这些决策(树枝)有相同的父节点
            //这些值相同的相邻树枝,只遍历第一次出现的即可,否则会出现重复解
            if(i > start && candidates[i] == candidates[i -1]){  
                continue;
            }
            //处理节点
            path.push_back(candidates[i]);
            sum += candidates[i];
            //递归遍历下一层回溯树
            backtrack(candidates, target, i+1);
            //撤销选择
            path.pop_back();
            sum -= candidates[i];
        }

    }
    vector<vector<int>> combinationSum2(vector<int>& candidates, int target) {
        sort(candidates.begin(), candidates.end()); // 先排序,让相同的元素靠在一起
        backtrack(candidates, target, 0);

        return res;
    }
};

3. 力扣39

该题特点:元素不重复,可重复选择。

例题1,2 都是通过控制递归函数的参数start 来确保不重复使用元素。这个 i 从 start 开始,那么下一层回溯树就是从 start + 1 开始,从而保证 nums[start] 这个元素不会被重复使用,那么反过来,如果想让每个元素被重复使用,只要把 i + 1 改成 i 即可:

class Solution {
public:
    vector<vector<int>> res;  // 记录所有可能的解
    vector<int> path;  // 记录一个可行解
    int sum;  // 记录路径和,方便我们判断递归边界
    // 回溯算法主函数
    void backtrack(vector<int>& candidates, int target, int start){
        // 终止条件1:找到能凑成target的组合时,没必要再往下找,再往下一定比 target大
        if(sum == target){
            res.push_back(path);
            return;
        }
        // 终止条件2:当前组合值比 target大,没必要再往下找,再往下一定比 target大
        if(sum > target) return;

        // 回溯算法标准框架
        for(int i = start; i < candidates.size(); i++){
            // 处理节点
            path.push_back(candidates[i]);
            sum += candidates[i];
            // 递归遍历下一层回溯树
            backtrack(candidates, target, i);
            //撤销选择
            path.pop_back();
            sum -= candidates[i];
        }
    }
    vector<vector<int>> combinationSum(vector<int>& candidates, int target) {
        backtrack(candidates, target, 0);
        return res;
    }   
};

3. 排列型

在乎顺序:1,2,3 和 1,3,2 是两种不同的情况。在空间树中体现为一颗左右子树高度相同的一颗多叉树。递归函数中需使用一个额外的形参 used 数组,标记已经在路径上的元素(已经被选择过的元素),避免重复选择。例如第1题中,因为是排列问题,如果第一步决策选择了2,第二步决策可以选择1,也可以选择3,所以用 used数组 来标记 已经做过的决策

1. 力扣46 全排列

该题特点:元素不可重复,不可重复选择。

本题求解的是到状态空间树叶子节点的路径。

class Solution {
public:
    vector<vector<int>> res;  // 记录所有的可行解
    vector<int> path;   // 记录回溯算法的递归路径
    // 回溯算法核心函数,遍历子集问题的状态空间树
    void backtrack(vector<int>& nums, vector<bool>& used){
        if(path.size() == nums.size()){  // 递归终点:到达叶子节点
            res.push_back(path);   // 将到达叶子节点的路径存放到结果集中并返回
            return;
        }
        // 回溯算法标准框架
        for(int i = 0; i < nums.size(); i++){
            // 已经在决策中被选择的元素,不能重复选择
            if(used[i] == true) continue;
            // 处理在决策中未被选择的节点
            path.push_back(nums[i]);
            used[i] = true;
            // 进入状态空间树的下一层递归
            backtrack(nums, used);
            // 撤销选择,进行回溯
            path.pop_back();
            used[i] = false;
        }
    }
    vector<vector<int>> permute(vector<int>& nums) {
        vector<bool> used(nums.size(), false);
        backtrack(nums, used);
        return res;
    }
};

2. 力扣47

该题特点:元素可重复,不可重复选择。【需要去重,先排序后剪枝:保持元素的相对位置不变】

class Solution {
public:
    vector<vector<int>> res;  // 记录所有的可行解    
    vector<int> path;  // 记录回溯算法的递归路径
    // 回溯算法核心函数,遍历子集问题的状态空间树, used数组:记录元素是否被使用
    void backtrack(vector<int>& nums, vector<bool>& used){ 
        if(path.size() == nums.size()){ // 递归终点:到达叶子节点
            res.push_back(path);
            return;
        }
        // 回溯算法标准框架
        for(int i = 0; i < nums.size(); i++){
            // 已经在决策中被选择的元素,不能重复选择
            if(used[i] == true) continue;
            // 剪枝逻辑,固定相同的元素在排列中的相对位置:前面的元素没有被使用则后面的元素也不能使用
            // 后面元素先出现一定会产生重复值,见上图
            if(i > 0 && nums[i] == nums[i -1] && !used[i -1]){
                continue;
            }
            // 处理节点
            path.push_back(nums[i]);
            used[i] = true;
            // 进入状态空间树的下一层递归
            backtrack(nums, used);
            // 撤销选择,进行回溯
            path.pop_back();
            used[i] = false;

        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        vector<bool> used(nums.size(), false);  // 排序,让相同的元素靠在一起

        sort(nums.begin(), nums.end());
        backtrack(nums, used);

        return res;
    }
};

        

3.  补充题

该题特点:元素不重复,可重复选择。

比如输入 nums = [1,2,3],那么这种条件下的全排列共有 3^3 = 27 种:

[
  [1,1,1],[1,1,2],[1,1,3],[1,2,1],[1,2,2],[1,2,3],[1,3,1],[1,3,2],[1,3,3],
  [2,1,1],[2,1,2],[2,1,3],[2,2,1],[2,2,2],[2,2,3],[2,3,1],[2,3,2],[2,3,3],
  [3,1,1],[3,1,2],[3,1,3],[3,2,1],[3,2,2],[3,2,3],[3,3,1],[3,3,2],[3,3,3]
]

标准的全排列算法利用 used 数组进行剪枝,避免重复使用同一个元素。如果允许重复使用元素的话,去除所有 used 数组的剪枝逻辑就行了。

class Solution {
public:
    vector<vector<int>> res;  // 记录所有的可行解    
    vector<int> path;  // 记录回溯算法的递归路径
    // 回溯算法核心函数,遍历子集问题的状态空间树, used数组:记录元素是否被使用
    void backtrack(vector<int>& nums){ 
        if(path.size() == nums.size()){ // 递归终点:到达叶子节点
            res.push_back(path);
            return;
        }
        // 回溯算法标准框架
        for(int i = 0; i < nums.size(); i++){
            // 处理节点
            path.push_back(nums[i]);
            // 进入状态空间树的下一层递归
            backtrack(nums);
            // 撤销选择,进行回溯
            path.pop_back();
        }
    }
    vector<vector<int>> permuteUnique(vector<int>& nums) {
        backtrack(nums);
        return res;
    }
};

力扣51 n皇后

class Solution {
public:
    vector<vector<string>> res;  // 存放最终结果,满足条件的棋盘

    // 判断皇后能否放在 (row, col)
    bool isValid(vector<string>& board, int row, int col){
        // 比较前 row -1 行已经放置的皇后 /
        // 判断即将放置的皇后和已经存在了的皇后 是否处于同一列
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q')
                return false;
        }
        int leng = board.size(); 
        // 判断即将放置的皇后和已经存在了的皇后 是否处于同一斜线
        // 1. 检查右上方是否有皇后与即将放置的皇后处于同一斜线
        for(int i = row -1, j = col + 1; i >= 0 && j < leng; i--, j++){
            if(board[i][j] == 'Q') return false;
        }
        // 2. 检查左上方是否有皇后与即将放置的皇后处于同一斜线
        for(int i = row -1, j = col - 1; i >= 0 && j >= 0; i--, j--){
            if(board[i][j] == 'Q') return false;
        }

        return true;
    }

    // 路径:board 中小于 row 的那些行都已经成功放置了皇后
    // 选择列表:第 row 行的所有列都是放置皇后的选择
    // 结束条件:row 超过 board 的最后一行
    void backtrack(vector<string>& board, int row){
        // 所有行都放了皇后,递归终止(递归边界)
        if(row == board.size()){   
            res.push_back(board);
            return;
        }
        //找出第 row 行能放 皇后的位置
        int leng = board[row].size();
        for(int col = 0; col < leng; col++){
            // 不满足条件的排除掉:同列,同斜线
            if(!isValid(board, row, col)) 
                continue;

            // 处理节点
            board[row][col] = 'Q';
            // 进入下一行决策
            backtrack(board, row + 1);
            // 撤销选择,回溯
            board[row][col] = '.';
        }

    }
    vector<vector<string>> solveNQueens(int n) {
        // '.' 表示空,'Q' 表示皇后,初始化空棋盘
        vector<string> board(n, string(n, '.'));  // vector<string> 代表一个棋盘
        backtrack(board, 0);
        return res;
    }
};

力扣52

class Solution {
public:
    int count = 0;
    // 判断皇后能否放在 (row, col)
    bool isValid(vector<string>& board, int row, int col){
        // 比较前 row -1 行已经放置的皇后 /
        // 判断即将放置的皇后和已经存在了的皇后 是否处于同一列
        for (int i = 0; i < row; i++) {
            if (board[i][col] == 'Q')
                return false;
        }
        int leng = board.size(); 
        // 判断即将放置的皇后和已经存在了的皇后 是否处于同一斜线
        // 1. 检查右上方是否有皇后与即将放置的皇后处于同一斜线
        for(int i = row -1, j = col + 1; i >= 0 && j < leng; i--, j++){
            if(board[i][j] == 'Q') return false;
        }
        // 2. 检查左上方是否有皇后与即将放置的皇后处于同一斜线
        for(int i = row -1, j = col - 1; i >= 0 && j >= 0; i--, j--){
            if(board[i][j] == 'Q') return false;
        }

        return true;
    }
    void backtrack(vector<string>& board, int row){
    // 所有行都放了皇后,递归终止(递归边界)
    if(row == board.size()){   
        count++;
        return;
    }
    //找出第 row 行能放 皇后的位置
    int leng = board[row].size();
    for(int col = 0; col < leng; col++){
        // 不满足条件的排除掉:同列,同斜线
        if(!isValid(board, row, col)) 
            continue;

        // 处理节点
        board[row][col] = 'Q';
        // 进入下一行决策
        backtrack(board, row + 1);
        // 撤销选择,回溯
        board[row][col] = '.';
    }

}
    int totalNQueens(int n) {
        vector<string> board(n, string(n, '.'));
        backtrack(board, 0);
        return count;
    }
};

3. DFS、BFS算法

DFS -- 栈 -- 递归(回溯)

BFS -- 队列 -- 层序遍历

1. 走迷宫:基于递归的DFS伪代码

vis[][] // vis[x][y] 标记 (x, y)是否已经访问

DFS(int x, int y){  // 当前正在访问的点是 (x, y)
    if (x, y)是终点   // 边界
        走到终点
        return       // 返回
    
    vis[x][y] = true  // 标记点(x, y) 已访问
    for 点(x, y)的上下左右点(nx, ny)
        if (nx, ny) 没越界 && (nx, ny) 未访问 && (nx, ny) 不是墙
            DFS(nx, ny)  // 递归访问点 (nx, ny)
 } 

二叉树的先序遍历:

class Solution {
public:
    void travel(TreeNode* root, vector<int>& result){
        if(root == nullptr) return;
        result.push_back(root -> val);
        traverl(root-> left, result);
        traverl(root-> right, result);
    }
    vector<int> preorderTraversal(TreeNode* root) {
        vector<int> result;
        travel(root, result);
        return result;
    }
};

2. 走迷宫:基于队列的BFS伪代码(类似于二叉树的层序遍历)

inq[][]  // inq[x][y] 标记点 (x, y)是否已经入过队
q  // 队列

BFS(int x, int y){
    q.push((x, y))  // (x, y)入队
    inq[x][y] = true  // 标记(x, y)入队 
    // while 循环控制一层一层往下走,for 循环利用q.size()控制从左到右遍历每一层二叉树节点:
    while q 不为空:
        取出队首元素(tx, ty)
        if (tx, ty)是终点  // 边界
            走到终点
            return  // 返回
        for 点(tx, ty)的邻居(nx, ny)
            if(nx, ny)没越界 && (nx, ny)未访问 && (nx, ny)不是墙
                q.push((nx, ny))
                inq[nx, ny];
    // 队列为空,搜索结束,说明没有找到  
}

二叉树的层序遍历:

class Solution {
public:
    vector<vector<int>> levelOrder(TreeNode* root) {
        queue<TreeNode*> q; //该队列是为了遍历该树
        if(root != nullptr) q.push(root);

        vector<vector<int>> result; // 存放遍历该树的结果,是每层遍历结果的组合

        while(!q.empty()){
            int size = q.size(); //统计每层节点的个数,顶层只有一个节点
            vector<int> vec; // 存放每层遍历的结果

            for(int i = 0; i < size; i++){ //弹出每层节点,加入下一层节点
                TreeNode* cur = q.front();
                q.pop();  //得到父节点(上一层节点)并弹出,接下来加入该父节点的 子节点(即 下一层节点)
                vec.push_back(cur-> val);  // 将处于同一层的节点的值用 vec 保存
                if(cur-> left) q.push(cur-> left);
                if(cur-> right) q.push(cur-> right);
            }

            result.push_back(vec);
        }

        return result;
    }
};

可以发现,图的遍历与树的遍历是很相似的,由于树是特性,它不会重复访问一个节点(无环),所以不需要 vis 数组记录某个节点是否被访问,而图就必须要使用 vis 数组记录。

3. 图的遍历伪码

图的存储常见的有邻接表和邻接矩阵。c++语法如下

// 邻接表(能够知道图中某点的所有邻接点)
// graph[x] 存储 x 的所有邻居节点以及对应的权重
vector<pair<int, int>> graph[];

// 邻接矩阵(能够快速知道图中两点间是否有边,耗费空间)
// matrix[x][y] 记录 x 指向 y 的边的权重,0 表示不相邻
vector<vector<int>> matrix;

下文采用的类似于c的

DFS

const int N = 2e5 + 10;  // 2 * 10^5 + 10
vector<int> garph[N]; // 邻接表:行数由N确定,列数自动扩容
bool vis[N];  //标记节点是否被访问

// 以顶点 u 为起点遍历
void DFS(int u){
	vis[u] = true;  // 标记 u 被访问过
	cout << u << endl;  // 访问 顶点 u

	for(int i = 0; i < graph[u].size(); i++){
		// 遍历 u 能走到的所有点
		int v = graph[u][i];
		if(!vis[v]){  //  没有被访问过
			DFS(v);
		}
	}

	return;
}

图可能不连通,则分别遍历每个连通块:

// 多个连通块
for(int i = 1; i <= n; i++){
    if(!vis[i]){
        DFS(i);
    }
}

BFS

const int N = 1e5 + 10;
vector<int> graph[N];
bool inq[N];  // 标记节点是否入队

void BFS(int u){
	queue<int> q;
	q.push(u);  // 起点入队
	inq[u] = true;
	
	while(!q.empty()){
		int t = q.front();
		q.pop();
		cout << t << endl;  // 访问队首
		// 查看队首的所有邻居
		for(int i = 0; i < graph[t].size(); i++){
			int v = graph[t][i];
			if(!inq[v]){
				q.push(v);
				inq[v] = true;
			}
		}
	}
}

图可能不连通,则分别遍历每个连通块:

// 多个连通块 
for(int i = 1; i <= n; i++){
	if(!inq[i]){
		BFS(i);
	}
}

力扣797

由于输入的图是有向无环的,我们就不需要 vis 数组辅助

class Solution {
public:
    // 记录所有路径
    vector<vector<int>> res;
    vector<int> path;
    /* 图的遍历框架:本质是DFS */
    void traverse(vector<vector<int>>& graph, int s) {
        // 添加节点 s 到路径
        path.push_back(s);

        int n = graph.size();
        if (s == n - 1) {
            // 到达终点
            res.push_back(path);
            // 可以在这直接 return,但要正确维护 path
            // path.pop_back();
            // return;
            // 不 return 也可以,因为图中不包含环,不会出现无限递归
        }

        // 递归每个相邻节点
        for (auto& v : graph[s]) {
            traverse(graph, v);
        }
        
        // 从路径移出节点 s
        path.pop_back();
    }
    vector<vector<int>> allPathsSourceTarget(vector<vector<int>>& graph) {  
        traverse(graph, 0);
        return res;
    }
};

2. 例题

以下题目代码不在是力扣模式,而是ACM模式,请自行区分两种方式的不同

1. 迷宫的判定

DFS
#include <bits/stdc++.h>

using namespace std;

const int N = 50; // 1 <= n, m <= 40
int n, m;  // 迷宫的行数和列数
char mp[N][N];  // 存放迷宫
bool flag;  // 标记是否能够从起点到达终点,默认值为false
bool vis[N][N];  // 标记是否访问过某节点,默认值为false
int dir[4][2] = {-1, 0, 0, 1, 1, 0, 0, -1}; // 方向数组: {-1, 0}:上, {0, 1}:右, {1, 0}:下, {0, -1}:左

bool in(int x, int y){   // 判断 (x, y)是否越界
    return x >= 1 && x <= n && y >= 1 && y <= m;
}

// 与前面回溯法类似的代码框架
void DFS(int x, int y){  //正在访问点 (x, y)
    // 递归边界
    if(x == n && y == m){   // 到达终点将标记设置为 true 后返回
        flag = true;
        return;
    }
    
    vis[x][y] = true;  // 标记已经访问过的节点
    //递归调用,找上下左右相邻点,符合条件的递归进行访问
    for(int i = 0; i < 4; i++){
        // 处理节点
        int nx = x + dir[i][0];
        int ny = y + dir[i][1];
        // (nx, ny) 未越界 && 未访问 && 不是障碍物
        if(in(nx, ny) && !vis[nx][ny] && mp[nx][ny] != '#'){
            DFS(nx, ny);  // 递归访问(nx, ny)
        }
    }
    //如果有需要,会在此处进行回溯
}

int main(){
    cin >> n >> m;  // 迷宫的行数和列数
    // 初始化迷宫:这样初始化是为了方便处理
    for(int i = 1; i <= n; i++){
        for(int j = 1; j <= m; j++){
            cin >> mp[i][j];
        }
    }
    //从(1, 1)开始进行深度优先搜索,能走到右下角(n, m)则 flag 为 true
    DFS(1, 1);
    if(flag){
        cout << "YES" << endl;
    }else{
        cout << "NO" << endl;
    }

    return 0;
}
BFS
#include<bits/stdc++.h>
using namespace std;

const int N = 50;  // 1 <= n, m <= 40
int n, m;  // 迷宫的行数和列数
char mp[N][N];  // 存放迷宫  
bool inq[N][N];  // 标记节点是否已经入过队
int dir[4][2] = {{-1, 0}, {0, 1}, {1,0}, {0,-1}};  // 方向数组: {-1, 0}:上, {0, 1}:右, {1, 0}:下, {0, -1}:左

struct node
{
	int x,y;
};

bool in(int x, int y)  // 判断点(x, y)是否越界
{
	return x >= 1 && x <= n && y >= 1 && y <=m;
}

void BFS(int x, int y)  //正在访问点 (x, y)
{
	queue<node> q;  // 定义 node 类型的队列
	q.push({x, y});  // 起点入队
    inq[x][y] = true;  // 标记起点已入队

	while(!q.empty())  // 队列非空时,对队列的临界点进行操作
	{
		node t = q.front();  // 取出队首元素,是node类型的变量
		q.pop();  // 队首出队(删除队首)
        // 找到队首的上下左右临界点,符合条件的入队,并标记
		for(int i = 0; i < 4; i++)
		{
			int nx = t.x + dir[i][0];
			int ny = t.y + dir[i][1];
            // (nx, ny)未越界 && 未入队 && 不是障碍物
			if (in(nx, ny) && !vis[nx][ny] && mp[nx][ny] != '#')
			{
				q.push({nx, ny});  // (nx, ny)入队
				inq[nx][ny] = true;  // 标记 (nx, ny)已入队
			}
		}
	}
}

int main()
{
	cin >> n >> m;  // 迷宫的行数和列数
    // 初始化迷宫:这样初始化是为了方便处理
	for(int i = 1; i <= n; i++)
	{
		for(int j= 1; j<= m; j++)
		{
			cin >> mp[i][j];
		}
	}
	BFS(1, 1);

	if(inq[n][m]){   // vis[n][m] = true, 表示(n, m)点被访问
        cout << "YES" << endl;
    }else{
        cout << "NO" << endl;
    }

	system("pause");
	return 0;
}

2. 迷宫之路径(DFS)

相比于第一题的DFS,区别有两点:

  1. 多了一个 vector<node> v; 用来存放起点到终点某条路径上经过的所有节点。
  2. 多了一个撤销选择的步骤(回溯)
#include <bits/stdc++.h>
using namespace std;

const int N = 20;  // 1 < n, m < 15
int m, n;  // 迷宫的行数和列数
int mp[N][N];  // 存迷宫 
int sx, sy, ex, ey; // (sx, sy)起点, (ex, ey)终点
bool vis[N][N];  // 标记数组,标记起点到终点路径中的节点是否已被访问
bool flag;  // 如果起点到终点间有路径,flag设置为 true
int dir[4][2] = {0, -1, -1, 0, 0, 1, 1, 0};  //左上右下
struct node{  
    int x, y;
};
vector<node> path;  // 存储路径:起点到终点路径中经过的节点

bool in(int x, int y){
    return x >= 1 && x <= m && y >= 1 && y <= n;
}

// 走迷宫,然后把所有路径都输出 
void DFS(int x, int y){  // 正在访问 (x, y) 
    if(x == ex && y == ey){   // 到达终点,将当前路径输出 
        // 输出路径上的所有点
        for(int i = 0; i < path.size(); i++){
            printf("(%d,%d)->", path[i].x, path[i].y);
        }
        printf("(%d,%d)\n", ex, ey);  // 终点
        flag = true;  // 标记从起点到终点有路径 

        return;  // 递归边界,返回 
    }

    vis[x][y] = true;  // 标记 (x, y) 已访问
    path.push_back({x, y});   // 把 (x, y) 放到路径中
    // 访问 (x, y)的邻节点
    for(int i = 0; i < 4; i++){
        int nx = x + dir[i][0];
        int ny = y + dir[i][1];
        // 未越界 && 未访问 && 可以走
        if(in(nx, ny) && !vis[nx][ny] && mp[nx][ny] == 1){
            DFS(nx, ny);  // 递归访问next (nx, ny) 
        }
    }
    // 回溯 
    vis[x][y] = false;
    path.pop_back();
}

int main(){
    cin >> m >> n;
    for(int i = 1; i <= m; i++){
        for(int j = 1; j <= n; j++){
            cin >> mp[i][j];
        }
    }
    cin >> sx >> sy;  // start(x, y)
    cin >> ex >> ey;  // end(x, y)
    DFS(sx, sy);   // 从起点出发搜索所有到终点的路径,并且输出路径 
    if(!flag) cout << -1 << endl;  // 从起点无法到达终点 

    system("pause");
    return 0;
}

3. 迷宫之最短路径(BFS)

#include <bits/stdc++.h>

using namespace std;
const int N = 50;
char mp[N][N];  // 迷宫
int r,c;  // 迷宫的行数和列数
bool inq[N][N];  // 标记点(x, y)是否已入队
int dir[4][2] = {{-1, 0}, {0, 1}, {1,0}, {0,-1}};  // 方向数组
struct node{
    int x, y, d;  // 到达点 (x, y)的最短步数是 d
};

bool in(int x, int y) {  // 判断是否未越界,如果未越界,返回 true,否则返回 false 
    return x >= 1 && x <= r && y >= 1 && y <= c;
}

// 从左上角 (1, 1) 到右下角 (r, c) 的最短步数(走不到则返回 -1) 
int BFS(int x, int y){  // (x, y) 是起点 
    queue<node> q;  
    q.push({x, y, 0});  // 起点入队,起点到起点是 0 步
    inq[x][y] = true;  // 标记起点已入队

    while(!q.empty()){ // 当队列非空 
        node t = q.front();  // 取出队首
        q.pop();  // 队首出队(删除队首)

        // 1. 队首是终点,返回最短步数
        if(t.x == r && t.y == c) return t.d;  // 到(t.x, t.y)的最短步数是 t.d
        // 找队首的上下左右相邻点,符合条件的入队,并标记
        for(int i = 0; i < 4; i++){
            int nx = t.x + dir[i][0];
            int ny = t.y + dir[i][1];
              // 未越界 && 未入队 && 不是墙
            if(in(nx, ny) && !vis[nx][ny] && mp[nx][ny] != '#'){
                q.push({nx, ny, t.d + 1});  //next (nx, ny)入队,到达(nx, ny)的最短路径是 到达(x, y)的最短路径 + 1
                inq[nx][ny] = true;  // 标记(nx, ny)已入队
            }
        }
    }
    return -1;
}

int main(){
    cin >> r >> c; 

    for(int i = 1;  i <= r; i++){
        for(int j = 1; j <= c; j++){
            cin >> mp[i][j];
        }
    }

    int ans = BFS(1, 1);
    cout << ans << endl;

    system("pause");
    return 0;
}


4. 力扣200 岛屿数量

DFS
class Solution {
private:
    int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 方向数组
    void dfs(vector<vector<char>>& grid, vector<vector<bool>>& vis, int x, int y) {
        if (vis[x][y] || grid[x][y] == '0') return; // 终止条件:访问过的节点 或者 遇到海水

        vis[x][y] = true; // 标记访问过
        for (int i = 0; i < 4; i++) {
            int nx = x + dir[i][0];
            int ny = y + dir[i][1];
            if (nx < 0 || nx >= grid.size() || ny < 0 || ny >= grid[0].size()) continue;  // 越界了,直接跳过
            dfs(grid, vis, nx, ny);
        }
    }
public:
    int numIslands(vector<vector<char>>& grid) {
        int n = grid.size(), m = grid[0].size();
        vector<vector<bool>> vis(n, vector<bool>(m, false));

        int result = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (!vis[i][j] && grid[i][j] == '1') {
                    result++;  // 遇到没访问过的陆地,+1(没有访问过的一定是不相连的,因为相连的点在第一次递归的时候vis数组就会标记为 ture,再次访问时不会进入这个判断语句)
                    dfs(grid, vis, i, j); // 将与其链接的陆地都标记上 true
                }
            }
        }
        return result;
    }
};

BFS
class Solution {
private:
int dir[4][2] = {0, 1, 1, 0, -1, 0, 0, -1}; // 方向数组
void bfs(vector<vector<char>>& grid, vector<vector<bool>>& inq, int x, int y) {
    queue<pair<int, int>> que;
    que.push({x, y});
    inq[x][y] = true; // 只要加入队列,立刻标记 

    while(!que.empty()) {
        pair<int ,int> cur = que.front(); 
        que.pop();

        int curx = cur.first;
        int cury = cur.second;

        for (int i = 0; i < 4; i++) {
            int nx = curx + dir[i][0];
            int ny = cury + dir[i][1];
            if (nx < 0 || nx >= grid.size() || ny < 0 || ny >= grid[0].size()) continue;  // 越界了,直接跳过
            if (!inq[nx][ny] && grid[nx][ny] == '1') {
                que.push({nx, ny});
                inq[nx][ny] = true; // 只要加入队列立刻标记
            }
        }
    }
}
public:
    int numIslands(vector<vector<char>>& grid) {
        int n = grid.size(), m = grid[0].size();
        vector<vector<bool>> inq = vector<vector<bool>>(n, vector<bool>(m, false));

        int result = 0;
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                if (!inq[i][j] && grid[i][j] == '1') {
                    result++; // 遇到没访问过的陆地,+1
                    bfs(grid, inq, i, j); // 将与其链接的陆地都标记上 true
                }
            }
        }
        return result;
    }
};

  • 16
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
逻辑结构:描述数据元素之间的逻辑关系,如线性结构(如数组、链表)、树形结构(如二叉树、堆、B树)、结构(有向、无向等)以及集合和队列等抽象数据类型。 存储结构(物理结构):描述数据在计算机中如何具体存储。例如,数组的连续存储,链表的动态分配节点,树和的邻接矩阵或邻接表表示等。 基本操作:针对每种数据结构,定义了一系列基本的操作,包括但不限于插入、删除、查找、更新、遍历等,并分析这些操作的时间复杂度和空间复杂度。 算法算法设计:研究如何将解决问题的步骤形式化为一系列指令,使得计算机可以执行以求解问题。 算法特性:包括输入、输出、有穷性、确定性和可行性。即一个有效的算法必须能在有限步骤内结束,并且对于给定的输入产生唯一的确定输出。 算法分类:排序算法(如冒泡排序、快速排序、归并排序),查找算法(如顺序查找、二分查找、哈希查找),算法(如Dijkstra最短路径算法、Floyd-Warshall算法、Prim最小生成树算法),动态规划,贪心算法,回溯法,分支限界法等。 算法分析:通过数学方法分析算法的时间复杂度(运行时间随数据规模增长的速度)和空间复杂度(所需内存大小)来评估其效率。 学习算法与数据结构不仅有助于理解程序的内部工作原理,更能帮助开发人员编写出高效、稳定和易于维护的软件系统。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值