1 - DFS入门
思想
DFS算法,也称回溯算法,本质上就是一种暴力穷举算法。解决一个回溯问题,实际上就是一个决策树的遍历过程,站在决策树的一个节点上,只需要思考以下三个问题:
- 路径:已经做出的选择;
- 选择列表:当前可做出的选择;
- 结束条件:已经到达决策树底层,无法再做出选择。
DFS算法的代码框架如下:
result = []
def dfs(路径, 选择列表):
if 满足结束条件:
result.add(路径)
return
for 选择 in 选择列表:
做出选择
dfs(路径, 选择列表)
撤销选择
其核心就是for
循环里的递归,在递归调用前“做出选择”,在递归调用后“撤销选择”。
全排列问题
46. 全排列 - 力扣(LeetCode)
决策树
首先,画出该问题的决策树。例如求[1, 2, 3]
全排列的决策树如下:
那么就可以确定路径、选择列表和结束条件了:
- 路径:已经选择了的数字
- 选择列表:还可以做出选择的数字
- 结束条件:到达决策树底部,选择列表为空
写DFS代码
参考代码如下:
vector<vector<int>> paths;
void dfs(vector<int>& nums, vector<int>& path, vector<int>& used)
{
// 结束条件
if (nums.size() == path.size())
{
paths.push_back(path);
return;
}
for (int i = 0; i < nums.size(); ++i)
{
if (!used[i])
{
// 做出选择
path.push_back(nums[i]);
used[i] = 1;
// dfs
dfs(nums, path, used);
// 撤销选择
path.pop_back();
used[i] = 0;
}
}
}
vector<vector<int>> permute(vector<int>& nums) {
vector<int> path, used(nums.size(), 0);
dfs(nums, path, used);
return paths;
}
这里没有显式记录“选择列表”,而是使用used[i]
记录nums[i]
是否已被选择过。
N皇后问题
51. N 皇后 - 力扣(LeetCode)
52. N 皇后 II - 力扣(LeetCode)
思路一
可以按行枚举,枚举每一行皇后可以放到哪儿。然后边做边判断,该路径放的皇后有无冲突,有的话可以直接停止DFS,往回走。这里就用到了 剪枝 的思想,如果当前判断发现DFS没用了,则不必继续DFS,直接返回原来的状态。
画决策树
思路一的决策树如下:
写DFS代码
参考代码如下:
// 所有答案
vector<vector<string>> paths;
// 当前答案
vector<string> path;
// 皇后在列, 副对角线, 主对角线是否能放
vector<int> col, dg, udg;
void dfs(int u, int n)
{
// 结束条件
if (u == n)
{
paths.push_back(path);
return;
}
for (int i = 0; i < n; ++i)
{
// 检查皇后是否能放
if (!col[i] && !dg[u + i] && !udg[n - u + i])
{
// 做出选择
path[u][i] = 'Q';
col[i] = dg[u + i] = udg[n - u + i] = 1;
dfs(u + 1, n);
// 撤销选择
path[u][i] = '.';
col[i] = dg[u + i] = udg[n - u + i] = 0;
}
}
}
vector<vector<string>> solveNQueens(int n) {
// 初始化
col.resize(20, 0);
dg.resize(20, 0);
udg.resize(20, 0);
path.resize(n, string(n, '.'));
dfs(0, n);
return paths;
}
这里用3个数组存储皇后是否能在列和两个对角线上存放的状态。有关这三个数组的位置关系推导如下:
如图所示,对于在棋盘(x, y)上的棋子,可以将它抽象为两个直线的交点,而对于棋盘上的每一个位置,这两个直线也是不一样的。因此可以用每组直线的截距去表示一个位置在列、两个对角线上存放的状态。
思路二
除了按行枚举外,还可以按棋盘上的每个格子枚举,分为在该格子上放/不放两种状态。
画决策树
写DFS代码
// 所有答案
vector<vector<string>> paths;
// 当前答案
vector<string> path;
// 皇后在行,列, 副对角线, 主对角线是否能放
vector<int> row, col, dg, udg;
void dfs(int x, int y, int u, int n)
{
// 每行走到最后一列, 变到下一行第一列
if (y == n)
{
++x;
y = 0;
}
// 结束条件: 到最后一行, 放了n个皇后
if (x == n)
{
if (u == n)
{
paths.push_back(path);
}
return;
}
// 做出不放皇后的选择, 不必撤销
dfs(x, y + 1, u, n);
if (!row[x] && !col[y] && !dg[x + y] && !udg[n + x - y])
{
// 做出放皇后的选择
path[x][y] = 'Q';
row[x] = col[y] = dg[x + y] = udg[n + x - y] = 1;
dfs(x, y + 1, u + 1, n);
// 撤销选择
path[x][y] = '.';
row[x] = col[y] = dg[x + y] = udg[n + x - y] = 0;
}
}
vector<vector<string>> solveNQueens(int n) {
// 初始化
row.resize(20, 0);
col.resize(20, 0);
dg.resize(20, 0);
udg.resize(20, 0);
path.resize(n, string(n, '.'));
dfs(0, 0, 0, n);
return paths;
}
总结
回溯算法就是一个多叉树的遍历问题,关键就是在前序遍历和后序遍历的位置做一些操作。
参考资料
- 《labuladong的算法笔记》
- 算法基础课 - AcWing