【算法】递归、回溯、剪枝、dfs 算法题练习(N皇后、单词搜索、数独问题;C++)

前言

我们知道:

DFS(深度优先搜索)是一种图遍历方法,通过尽可能深地访问图中的节点,然后回溯。可用于树的遍历、图的连通性检查等问题。
对于dfs,有以下常用方法:

  • 递归:算法的一种方法,通过函数自身调用来解决问题。通常用于解决具有重复子问题的情况,如树形结构或分治策略。

  • 回溯:一种递归的策略,用于遍历所有可能的解。通过在探索每一步时进行“尝试”和“撤销”操作来找到问题的所有解。常用于解决排列组合问题。

  • 剪枝:优化算法的一种技术,通过提前终止不必要的计算或分支,以减少运算量。常用于回溯算法中,通过检测并排除不符合条件的解。

一般对于这种题,可以通过画递归树,根据递归树写代码, 下面通过一系列的算法题加深对这种算法的理解:


算法题

1.N皇后

在这里插入图片描述
思路

递归树:

在这里插入图片描述
代码

class Solution {
public:
    // 创建3数组分别记录列和对角线的存储情况
    vector<bool> checkCol, checkDiagonal, checkSubDiagonal;
    vector<vector<string>> ret;
    vector<string> path;
    int _n;

    vector<vector<string>> solveNQueens(int n) {
        _n = n;
        checkCol.resize(n);  checkDiagonal.resize(2*n);   checkSubDiagonal.resize(2*n);
        // 将字符串先全部初始化为"."
        path.resize(n);
        for(int i = 0; i < n; ++i)
            path[i].append(n, '.');

        dfs(0);
        return ret;    
    }

    void dfs(int row)
    {
        // 记录结果
        if(row == _n)
        {
            ret.push_back(path);
            return;
        }

        for(int col = 0; col < _n; ++col)
        {
            if(!checkCol[col] && !checkDiagonal[row + col] && !checkSubDiagonal[row - col + _n])
            {
                path[row][col] = 'Q'; // 放入皇后
                checkCol[col] = checkDiagonal[row + col] = checkSubDiagonal[row - col + _n] = true; // 更新状态
                dfs(row + 1);
                // 恢复现场
                path[row][col] = '.';
                checkCol[col] = checkDiagonal[row + col] = checkSubDiagonal[row - col + _n] = false;
            }
        }
    }
};

2.有效的数独

在这里插入图片描述

思路

  1. 定义检查数组

    • checkRow[9][10]:检查每一行中数字1-9的出现情况。
    • checkCol[9][10]:检查每一列中数字1-9的出现情况。
    • checkGrid[9][9][10]:检查每个3x3的子网格中数字1-9的出现情况。
  2. 遍历数独板

    • 双重循环遍历每个单元格 board[i][j]
    • 如果单元格中的值不是 '.',即有数字 num,则进行如下检查:
      • 检查行 i、列 j、和对应的3x3子网格中是否已存在 num
      • 如果任意一个检查发现 num 已存在,则返回 false,说明数独无效。
      • 否则,将 num 标记为已存在。
  3. 返回结果

    • 如果遍历完成后没有发现重复数字,则返回 true,说明数独有效。

代码

class Solution {
public:
    // 创建数组,分布表用于检查行、列、对角线是否存在num
    bool checkRow[9][10], checkCol[9][10], checkGrid[9][9][10];

    bool isValidSudoku(vector<vector<char>>& board) {
        for(int i = 0; i < 9; ++i)
            for(int j = 0; j < 9; ++j)
            {
                if(board[i][j] != '.')
                {
                    int num = board[i][j] - '0'; // 获取该数字
                    if(checkRow[i][num] || checkCol[j][num] || checkGrid[i / 3][j / 3][num])
                    {
                        return false;                        
                    }
                    checkRow[i][num] = checkCol[j][num] = checkGrid[i / 3][j / 3][num] = true;
                }
            }

        return true;
    }
};

3.解数独

在这里插入图片描述

思路

上一题要求判断数独数组是否有效,本题要求解数独:

  1. 初始化

    • 通过遍历 board,填充 checkRow, checkCol, 和 checkGrid 数组以记录已存在的数字。
  2. dfs 函数

    • 遍历整个 board,找出空白位置(即 '.')。
    • 对每个空白位置,尝试填入 19 的数字。
    • 通过检查行、列和子网格中是否已经存在该数字来判断是否可以填入。
    • 如果某个数字可以填入,更新状态并递归调用 dfs 继续尝试填入下一个位置。
    • 如果递归成功,返回 true;否则,恢复现场(即回溯),然后继续尝试下一个数字。
    • 如果所有数字都试过且都不成功,则返回 false

代码

class Solution {
public:
    // 用于记录board中的存储情况
    bool checkRow[9][10], checkCol[9][10], checkGrid[9][9][10];

    void solveSudoku(vector<vector<char>>& board) {
        // 初始化数组
        for(int i = 0; i < 9; ++i)
        {
            for(int j = 0; j < 9; ++j)
            {
                // 将开始就存在的数字设为true
                if(board[i][j] != '.')
                {
                    int num = board[i][j] - '0';
                    checkRow[i][num] = checkCol[j][num] = checkGrid[i/3][j/3][num] = true;
                }
            }
        }

        dfs(board); // 递归
    }

    bool dfs(vector<vector<char>>& board)
    {
        // 遍历矩阵
        for(int i = 0; i < 9; ++i)
        {
            for(int j = 0; j < 9; ++j)
            {
                if(board[i][j] == '.')
                {
                    for(int num = 1; num <= 9; ++num) // 在空位 依次插入1 ~ 9
                    {
                        if(!checkRow[i][num] && !checkCol[j][num] && !checkGrid[i/3][j/3][num])
                        {
                            board[i][j] = num + '0';
                            checkRow[i][num] = checkCol[j][num] = checkGrid[i/3][j/3][num] = true;
                            if(dfs(board)) return true;
                            // 恢复现场
                            board[i][j] = '.';
                            checkRow[i][num] = checkCol[j][num] = checkGrid[i/3][j/3][num] = false;
                        }
                    }
                    return false; // 1~9都无法插入,这条路走不通,return false
                }
            }
        }
        return true;
    }
};

4.单词搜索

在这里插入图片描述

思路

  1. 初始化

    • visited 矩阵用于记录已经访问过的格子,防止重复访问。
    • dxdy 数组用于简化四个方向的移动操作。
  2. exist 函数

    • 遍历网格的每个位置,寻找单词的起始字符。
    • 如果找到起始字符,则启动 DFS 搜索。
  3. dfs 函数

    • 递归地探索所有可能的路径来匹配单词的每个字符。
    • 确保当前移动是合法的(在边界内、未访问过且字符匹配)。
    • 若匹配到单词的最后一个字符,则返回 true

代码

class Solution {
public:
    int m, n;
    vector<vector<bool>> visited;
    //bool visited[7][7];
    int dx[4] = {0, 0, -1, 1};
    int dy[4] = {-1, 1, 0, 0};

    bool exist(vector<vector<char>>& board, string word) {
        m = board.size(), n = board[0].size();
        visited.resize(m, vector<bool>(n)); // 初始化

        for(int i = 0; i < m; ++i)
        {
            for(int j = 0 ; j < n; ++j)
            {
                if(board[i][j] == word[0])
                {
                    visited[i][j] = true;
                    if(dfs(board, i, j, word, 1)) return true;
                    visited[i][j] = false; // 恢复现场
                }
            }
        }
        return false;
    }

    bool dfs(vector<vector<char>>& board, int i, int j, string& word, int pos) {
        if(pos == word.size()) return true;

        for(int k = 0; k < 4; ++k)
        {
            int _x = i + dx[k], _y = j + dy[k];
            if(_x < m && _x >= 0 && _y < n && _y >= 0 && !visited[_x][_y] && board[_x][_y] == word[pos])
            {
                visited[_x][_y] = true;
                if(dfs(board, _x, _y, word, pos + 1)) return true;
                visited[_x][_y] = false;
            }   
        }
        return false;
    }
};

5.黄金矿工

在这里插入图片描述

思路

  1. 初始化和设置

    • visited 矩阵用于跟踪已访问的格子,避免重复访问。
    • dxdy 数组用于表示四个方向的移动。
  2. getMaximumGold 函数

    • 遍历网格的每个位置,若当前位置的金量非零,则调用 DFS 函数进行深度搜索。
    • ret 记录找到的最大金量。
  3. dfs 函数

    • 递归地探索所有可能的路径来获取最大金量。
    • 更新 ret 为当前路径的最大值。
    • 恢复 visited 状态以进行其他路径的探索。

代码

class Solution {
public:
    vector<vector<bool>> visited;
    int m, n, ret = 0;

    int getMaximumGold(vector<vector<int>>& grid) {
        m = grid.size(), n = grid[0].size();   
        visited.resize(m, vector<bool>(n));
        // 遍历矩阵
        for(int i = 0; i < m; ++i)
        {
            for(int j = 0; j < n; ++j)
            {
                if(grid[i][j])
                {
                    visited[i][j] = true;
                    dfs(grid, i, j, grid[i][j]);
                    // 恢复现场
                    visited[i][j] = false;
                }
            }
        }

        return ret;
    }

    int dx[4] = {0, 0, -1, 1};
    int dy[4] = {-1, 1, 0, 0};
    void dfs(vector<vector<int>>& grid, int i, int j, int path)
    {
        ret = max(ret, path);
        for(int k = 0; k < 4; ++k)
        {
            int _x = i + dx[k], _y = j + dy[k];
            // 如果找到未检索过且不为0的数字:
            if(_x >= 0 && _x < m && _y >= 0 && _y < n && !visited[_x][_y] && grid[_x][_y])
            {
                visited[_x][_y] = true;
                dfs(grid, _x, _y, path + grid[_x][_y]);
                // 恢复现场
                visited[_x][_y] = false;
            }
        }
    }

};


6.不同路径III

在这里插入图片描述

思路

  1. 初始化

    • visited 矩阵用于跟踪已访问的格子。
    • dxdy 数组表示移动的四个方向。
    • count 用于统计需要访问的空格的数量。
  2. uniquePathsIII 函数

    • 遍历网格,找到起点和统计要走的步数 (count),然后调用 dfs 函数。
  3. dfs 函数

    • 递归地遍历网格。
    • 当遇到终点 (2) 时,检查是否走完所有空格 (steps == 0),如果是,则路径有效。
    • 递归调用每个方向,并在返回时恢复访问状态。

代码

class Solution {
public:
    vector<vector<bool>> visited;
    int m, n, ret = 0, count = 0 ;
    int dx[4] = {0, 0, -1, 1};
    int dy[4] = {-1, 1, 0, 0};

    int uniquePathsIII(vector<vector<int>>& grid) {
        m = grid.size(), n = grid[0].size();
        visited.resize(m, vector<bool>(n));
        int start_i, start_j;

        for(int i = 0; i < m; ++i) 
            for(int j = 0; j < n; ++j) {
                if(grid[i][j] == 0) count++; // 统计0的个数, 即要走的步数
                else if(grid[i][j] == 1) {
                    start_i = i;
                    start_j = j;
                }
            }

        dfs(grid, start_i, start_j, count + 1);  // count+1 是因为要包括起始位置的步数
        return ret;
    }

    void dfs(vector<vector<int>>& grid, int i, int j, int steps) {
        if(grid[i][j] == 2) {
            if (steps == 0) ret++;  // 到达结束位置且所有空方格都被访问过
            return;
        }

        visited[i][j] = true;
        steps--;  // 每次访问一个空方格,剩余步数减一

        for(int k = 0; k < 4; ++k)
        {
            int _x = i + dx[k], _y = j + dy[k];
            if(_x >= 0 && _x < m && _y >= 0 && _y < n && !visited[_x][_y] && grid[_x][_y] != -1)
                dfs(grid, _x, _y, steps);
        }

        visited[i][j] = false;  // 回溯,重置访问状态
    }
};


  • 17
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值