DFS(Depth-First Search)也称深度优先搜索或深度优先遍历,有时又会被称为暴力搜索。这是一种利用图和树遍历的算法,深度优先搜索算法的思想很简单,就是从起点开始,不停地向下遍历,直到找到终点或者无法继续向下遍历为止。换言之,它是一种尽可能深地遍历图或树的算法。在实现DFS算法时,我们需要使用一个栈来保存当前节点的信息,然后在访问完当前节点后,将下一个需要访问的节点入栈,然后继续遍历下去。
本质
究其本质还是递归思想和回溯思想的结合。递归就是一个过程或函数在其定义中直接或间接调用自身的一种方法,他通常把一个的大型复杂的问题层层转化为一个与原问题相似的规模较小的问题来求解,通过这种方法只需少量的程序即可描述出需要多次重复计算的过程,,大大减少了代码量。回溯是确保深度搜索成功进行下去不可缺少的一环。算法搜索至解空间树的任一节点时,先判断该节点是否包含问题的解,如果不包含,则跳过对该节点为根的子树的搜索,逐层向其祖先节点回溯;否则进入该子树,继续按深度优先策略搜索。
要素
在做题DFS类型的题目时,要提取题目中的最关键的一个要素,同时也是串联起整个题目做题思路的关键要素,那就是——顺序。做题时,建立起一个树的模型,找清楚树的每个子节点和根节点所代表的意义,进而理清每一个节点的递推关系,思路便会打开,有利于代码实现。
典型例题
这是一道来自Acwing网站的典型算法基础题 https://www.acwing.com/problem/content/844/
排列数字
给定一个整数 n,将数字 1∼n 排成一排,将会有很多种排列方法。
现在,请你按照字典序将所有的排列方法输出。
输入格式
共一行,包含一个整数 n。
输出格式
按字典序输出所有排列方案,每个方案占一行。
数据范围
1≤n≤7
输入样例:
3
输出样例:
1 2 3 1 3 2 2 1 3 2 3 1 3 1 2 3 2 1
解题思路
题目描述的非常简单,也比较容易理解。第一眼看到题目时如果没有想到合适的解法思路,不妨先手动模拟一下,同时题目给的数据范围也非常小,我们可以一个个枚举出来,但是当测试案例大的时候,就需要运用dfs来循环遍历出来每一种情况。当n为3时,相当于现在有了3个空位,结合之前说过的"顺序"的重要性,我们不妨先从第一位数字开始一位一位枚举,第一位数字有三种选择,分别是1,2,3,先从1开始,此时开始选择第二个数字,因为1已经备选过了,所以只有两种选择2和3,在此选择2,并保存为一个节点。选了2之后,第三个数字就只剩一种选择3了,至此便得出了一种结果:1 2 3。可以将这个答案保存输出,之后便用到了回溯的思想, 当有一个答案输出后或者遍历无法进行下去时,便回到上个节点继续选择下一个可选的方案,在此处便回到刚才选了1之后的地方,由于2不能选了,则此处只能选择3,来到第三个数字,便只剩下2可选了,三个数都选择后又一个可行的方案:1 3 2便得到了。由此便再次回溯,回到第一个数字,这次选择2,便开始了一个新的分支,继续往下选择。
DFS类型的题目结合画图的方法可以更加直观的理清思路:
这张图便是我上述描述的作图表达。具体思路有了,剩下的便是利用代码具体实现。
用 path 数组保存排列,当排列的长度为 n 时,是一种方案,输出。
用 state 数组表示数字是否用过。当 state[i] 为 1 时:i 已经被用过,state[i] 为 0 时,i 没有被用过。
定义在全局变量下的布尔数组默认为false。
dfs(i) 表示的含义是:在 path[i] 处填写数字,然后递归的在下一个位置填写数字。
回溯:第 i 个位置填写某个数字的所有情况都遍历后, 第 i 个位置填写下一个数字。
题解代码
#include<iostream>
using namespace std;
const int N = 10;
int path[N];//保存序列
bool state[N];//数字是否被用过
int n;
void dfs(int u)
{
if(u == n)//数字填完了,输出
{
for(int i = 0; i < n; i++)//输出方案
cout << path[i] << " ";
cout << endl;
}
for(int i = 1; i <= n; i++)//空位上可以选择的数字为:1 ~ n
{
if(!state[i])//如果数字 i 没有被用过
{
path[u] = i;//放入空位
state[i] = true;//数字被用,修改状态
dfs(u + 1);//填下一个位
state[i] = false;//回溯,取出 i (还原现场)
}
}
}
int main()
{
cin >> n;
dfs(0); //引入的变量为已经填过数字的个数
}
有关DFS的一个非常经典的问题就是n皇后问题https://www.acwing.com/problem/content/845/
n-皇后问题
n−皇后问题是指将 n 个皇后放在 n×n 的国际象棋棋盘上,使得皇后不能相互攻击到,即任意两个皇后都不能处于同一行、同一列或同一斜线上。
现在给定整数 n,请你输出所有的满足条件的棋子摆法。
输入格式:
共一行,包含整数 n。
输出格式:
每个解决方案占 n 行,每行输出一个长度为 n 的字符串,用来表示完整的棋盘状态。
其中
.
表示某一个位置的方格状态为空,Q
表示某一个位置的方格上摆着皇后。每个方案输出完成后,输出一个空行。
注意:行末不能有多余空格。
输出方案的顺序任意,只要不重复且没有遗漏即可。
数据范围:
1≤n≤9
输入样例:
4
输出样例:
.Q.. ...Q Q... ..Q. ..Q. Q... ...Q .Q..
解题思路
这道题的解决思路与上题相似,都是对DFS的深入应用。
首先都是确定好我们做题进行数据枚举的顺序,先按行枚举,每行枚举一个皇后,然后判断在这个点上是否合法,合法便改为Q,继续枚举下一行,非法则继续枚举下一个点位,直到满足条件为止。这道题的一个难点就是代码实现,以及如何判断是否合法。在这里,第r行,第i列能不能放棋子:用数组dg(diagonal) udg(undiagonal) col(column) 分别表示:点对应的两个斜线以及列上是否有皇后。
dg[i + r] 表示 r行i列处,所在的对角线上有没有棋子,udg[n - i + r]表示 r行i列处,所在的反对角线上有没有棋子,col[i]表示第i列上有没有棋子。如果 r行i列的对角线,反对角线上都没有棋子,即!col[i] && !dg[i + r] && !udg[n - i + r]为真,则代表 r行i列处可以放棋子。
题解代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 20;
int n;
char g[N][N];//存储棋盘
bool col[N],dg[N],udg[N];//点对应的两个斜线以及列上是否有皇后
void dfs(int u)
{
if(u == n)//放满了棋盘,输出棋盘
{
for(int i = 0; i < n; i ++)
{
cout << g[i]<< endl;
}
cout << endl;
}
for (int i = 0; i < n; i ++ )
{
if(!col[i] && !dg[i+u] && !udg[i-u+n])//不冲突,放皇后
{
g[u][i] = 'Q';
col[i] = dg[i + u] = udg[i - u + n] = true;//对应的 列, 斜线 状态改变
dfs(u + 1);
col[i] = dg[i + u] = udg[i - u + n] = false;//恢复现场
g[u][i] = '.';
}
}
}
int main()
{
cin >> n;
for (int i = 0; i < n; i ++ )
{
for (int j = 0; j < n; j ++ )
{
g[i][j]= '.';
}
}
dfs(0);
return 0;
}
补充
提供几道DFS类型的题目可供来练习:
https://www.acwing.com/problem/content/94/
https://www.acwing.com/problem/content/96/
https://www.acwing.com/problem/content/95/
这些题目都是我在Acwing平台找到的,当然可以刷算法题的平台有很多,像洛谷,力扣等。