【算法】递归、回溯、剪枝、dfs 算法题练习(N皇后、单词搜索、数独问题;C++)
文章目录
- 前言
- 算法题
- 1.N皇后
- 2.有效的数独
- 3.解数独
- 4.单词搜索
- 5.黄金矿工
- 6.不同路径III
前言
我们知道:
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.有效的数独
思路
-
定义检查数组:
checkRow[9][10]
:检查每一行中数字1-9的出现情况。checkCol[9][10]
:检查每一列中数字1-9的出现情况。checkGrid[9][9][10]
:检查每个3x3的子网格中数字1-9的出现情况。
-
遍历数独板:
- 双重循环遍历每个单元格
board[i][j]
。 - 如果单元格中的值不是
'.'
,即有数字num
,则进行如下检查:- 检查行
i
、列j
、和对应的3x3子网格中是否已存在num
。 - 如果任意一个检查发现
num
已存在,则返回false
,说明数独无效。 - 否则,将
num
标记为已存在。
- 检查行
- 双重循环遍历每个单元格
-
返回结果:
- 如果遍历完成后没有发现重复数字,则返回
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.解数独
思路
上一题要求判断数独数组是否有效,本题要求解数独:
-
初始化:
- 通过遍历
board
,填充checkRow
,checkCol
, 和checkGrid
数组以记录已存在的数字。
- 通过遍历
-
dfs
函数:- 遍历整个
board
,找出空白位置(即'.'
)。 - 对每个空白位置,尝试填入
1
到9
的数字。 - 通过检查行、列和子网格中是否已经存在该数字来判断是否可以填入。
- 如果某个数字可以填入,更新状态并递归调用
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.单词搜索
思路
-
初始化:
visited
矩阵用于记录已经访问过的格子,防止重复访问。dx
和dy
数组用于简化四个方向的移动操作。
-
exist
函数:- 遍历网格的每个位置,寻找单词的起始字符。
- 如果找到起始字符,则启动 DFS 搜索。
-
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.黄金矿工
思路
-
初始化和设置:
visited
矩阵用于跟踪已访问的格子,避免重复访问。dx
和dy
数组用于表示四个方向的移动。
-
getMaximumGold
函数:- 遍历网格的每个位置,若当前位置的金量非零,则调用 DFS 函数进行深度搜索。
ret
记录找到的最大金量。
-
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
思路
-
初始化:
visited
矩阵用于跟踪已访问的格子。dx
和dy
数组表示移动的四个方向。count
用于统计需要访问的空格的数量。
-
uniquePathsIII
函数:- 遍历网格,找到起点和统计要走的步数 (
count
),然后调用dfs
函数。
- 遍历网格,找到起点和统计要走的步数 (
-
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; // 回溯,重置访问状态
}
};