DFS:深度优先搜索
-
DFS与BFS的对比
-
- DFS使用栈来实现,BFS使用队列来实现
-
- DFS所需要的空间是 O ( h ) O(h) O(h),而BFS需要的空间是 O ( 2 h ) O(2^h) O(2h),其中h是树的高度;
-
- DFS不具有最短路的特性,BFS有最短路的特性
DFS回溯的时候,一定要记得恢复现场
经典问题:全排列问题、n皇后问题
Acwing 842.排列数字
输入样例:
3
输出样例:
1 2 3
1 3 2
2 1 3
2 3 1
3 1 2
3 2 1
思路1:直接使用 C++标准库里面的next_permutation() 函数生成数组的下一个排列,直到所有排列生成完成。
具体代码:
#include <iostream>
#include <algorithm>
using namespace std;int main() {int n;cin >> n; // 输入 n// 初始化数组,从 1 到 nint num[n];for (int i = 0; i < n; i++) {num[i] = i + 1;}// 按照字典序输出所有排列do {for (int i = 0; i < n; i++) {cout << num[i] << " ";}cout << endl;} while (next_permutation(num, num + n)); // 使用 next_permutation 生成下一个排列return 0;
}
但此题还是要以练习DFS为主。
思路2:使用深度优先搜索,n = 3的图示如下:
- 每次搜索确实一个位置的数字,然后回溯判断是否有其它可能,回溯后要恢复现场(st[i] = false);
- 设置一个结果数组path[,存储每次确定放置的数字。dfs递归参数设置为u,表示要判断的第几个数字,若u=n表示找到一个序列输出,然后回溯(注意u从0开始,u=n时就意味着放置好了n个数)
-
- 设置一个bool数组st[],st[i] = true表示数字i已经放置,否则未放置;
-
- 若未放置,则将i放在当前位置path[u],且设置st[i] = true;
-
- 然后递归到下一个位置dfs(u+1),继续判断放置
-
- 到达最后一个位置输出一次结果后,回溯,恢复现场,设置st[i] = false,然后继续循环判断这个位置放另一个数;
具体代码(详解版)
#include <iostream>
#include <algorithm> using namespace std; const int N = 10;int n;
int path[N];//path存储结果(从0开始)
bool st[N];//st表示当前数字是否已经使用(放置)(从1开始)初始化默认为false//深度优先搜素
void dfs(int u){if(u == n){//u表示当前处理第几个数,u=n时已经处理n个for(int i = 0;i < n; i ++) cout << path[i] << ' ';cout << endl;return;}//否则继续搜索for(int i = 1; i <= n; i ++){if(!st[i]){//当前数字未被使用path[u] = i;//使用st[i] = true;dfs(u+1);//递归到下一个st[i] = false;//回溯后要恢复现场}}
}int main(){cin >> n;dfs(0);//从0开始return 0;
}
Acwing 843. n-皇后问题
输入样例:
4
输出样例:
.Q…
…Q
Q…
…Q.
…Q.
Q…
…Q
.Q…
思路分析:按行(u)枚举,dfs(u)
- 确定n个位置,每次需要回溯和剪枝(即发现条件不符合时直接跳过这种情形,而不是把这种情况完成在进行排除,从而减少了时间复杂度操作;
- 条件判断包括行、列、主对角线、副对角线。u表示目前放置第几个皇后,也表示第几行。列用i作为循环条件,每次确定一行中某个(u,i)放置皇后
-
- 对于行:由于每行只有一个皇后,要放置n个皇后,每次递归处理一行,放置一个皇后,u=n时,结束表示放置完毕,即行为隐藏编号,无需额外的行数组;
-
- 对于列:设置一个列数组col[],bool型,
col[i]
为true时表示当前列i已有皇后;
- 对于列:设置一个列数组col[],bool型,
-
- 对于主对角线:同样设置一个bool数组dg[],
dg[u+i]
为true时表示当前位置的主对角线方向有皇后;
- 对于主对角线:同样设置一个bool数组dg[],
-
- 对于副对角线:设置一个bool数组udg[],
udg[i-u+n]
为true时表示当前位置的副对角线方向有皇后;
- 对于副对角线:设置一个bool数组udg[],
关于为什么是
u+i
,i-u+n
:如果将棋盘类比成平面直角坐标系,左上角的点就是坐标原点O。可以把u看作横坐标,i看作纵坐标,若主对角线v1是不通过O的,那么v1上的点的横纵坐标之和不变,即u+i不变,副对角线v2上的点的横纵坐标之差不变即i-u不变,但是i-u可能会小于0(最小为0-8==-8),由于数组下标的限制,所以要对i-u加8。这样确保编号是正数,并且每条副对角线有唯一的编号,适合在数组中进行索引。(如下图)
- 注意每次回溯前,即放置皇后后,要设置当前列、主对角、副对角为true;回溯后要设置当前列、主对角、副对角为false(即回到当前位置要恢复现场)
具体代码实现(详解版):
#include <iostream>using namespace std;const int N = 20;int n;
char g[N][N]; // 用于存储棋盘
bool col[N], dg[N], udg[N]; // 列标记,主对角线标记,副对角线标记// 深度优先搜索函数
void dfs(int u) { // u 表示当前处理的行,从0开始,表示第一行if (u == n) { // 当 u == n 时,表示所有行都已经放置了皇后for (int i = 0; i < n; i++) puts(g[i]); // 输出当前棋盘,puts直接按行输出puts(""); // 输出空行return;}// 继续处理当前行的每一列for (int i = 0; i < n; i++) { // 遍历每一列if (!col[i] && !dg[u + i] && !udg[n - u + i]) { // 如果当前列、主对角线、副对角线都没有皇后g[u][i] = 'Q'; // 在当前格子放置皇后col[i] = dg[u + i] = udg[n - u + i] = true; // 更新列和对角线标记dfs(u + 1); // 递归处理下一行col[i] = dg[u + i] = udg[n - u + i] = false; // 回溯,恢复标记g[u][i] = '.'; // 移除皇后,恢复棋盘}}
}int main() {cin >> n; // 输入棋盘大小 n// 初始化棋盘,每个格子为空for (int i = 0; i < n; i++) {for (int j = 0; j < n; j++) {g[i][j] = '.';}}dfs(0); // 从0开始,表示第一行return 0;
}
总结:DFS 是一种强大且灵活的搜索算法,广泛应用于图搜索、树遍历、回溯法等问题中。通过深入每条可能路径来探索解,DFS 可以高效解决很多组合类问题。
DFS 通常通过递归实现,伪代码如下:
void dfs(节点 u) {if (u 是目标节点) {处理当前节点的结果;return;}for (每个与 u 相邻的节点 v) {if (v 未访问) {标记 v 为已访问;继续从 v 进行深度优先搜索;回溯后恢复 v 的状态; // 如果需要继续探索其他路径}}
}