dfs好久之前就想写一篇了,终于找到借口了 为了老弟来写一篇深度优先搜索吧!
基本概念
深度优先搜索(deepth first search)又叫dfs,基本思想是从给定点出发,选定一条路就一直走下去,不撞南墙不回头那种。不管你喜不喜欢,它就是这样专一。
一直走到头撞了南墙发现当前路径不可行的时候,才回头这时候的回头是通过 栈 + 回溯 来实现的,那么具体怎么搞↓
例题
dfs应用有很多,最基本的就是迷宫求路径数的问题。比如我们选择如下的简单迷宫↓从a开始走,每次只能上下左右四个方向走一格,直到b终点结束。求所有可以走的路。
算法思维
首先确认当前位置没有问题是可以走的。那么从当前位置出发,选一个方向开始试探,如果试探的这个方向可以走,就继续从这个新位置向下选个方向试探……直到撞了南墙就回头,或者到达迷宫出口为止
是不是很熟悉?对!每到一个位置我们要做的步骤都是一!样!的!所以我们可以通过递归来实现。
递归就必须考虑递归出口(也叫基线条件baseline、边界条件啥啥都行随你开心)我们的递归出口是啥?走到哪里就可以结束这个函数了?(其实上面已经说了)走到出口!
三个问题
看代码之前,停下来思考三个问题:
- 什么叫可以走?
- 如何选择试探的方向?
- 怎么回头?
回答
- 一个点可以走就是满足:(1)这个点在这个图中 (2)这个点是通路(在简单迷宫中,不是墙壁即可) 实现上用一个
if
判断一下就好 - 选择试探的方向,就是下一步应该往哪走有几种可能的选择,就要遍历这些选择(单个选择直接加上以后递归,多个选择用
for
循环来遍历所有) - 回头就是回溯,因为要回头说明你的这个点已经不可行了,那么就把你对这个位置做过的事情全部撤销。如果为了走这个位置,需要把这个位置标记成x,那就还原这个位置原来的状态。
比如我们的迷宫问题,从一个点可达的点就是它的上下左右四个位置所以我们遍历这四个位置。在一个用二维数组表示的图中如何实现?a点(x,y)的上面就是(x-1,y),下面是(x+1, y),左:(x, y-1) 右:(x, y+1)
当然你也可以用四个递归的调用语句来实现↓ 这里 g 表示图
g[x][y] = ' '; //标记通路
dfs(x + 1, y); //往下试探
dfs(x - 1, y); //往上试探
dfs(x, y + 1); //往右试探
dfs(x, y - 1); //往左试探
g[x][y] = 'o'; //回溯,撤销操作
取巧一点的办法(不想看就用上面那个法子试探,可以跳到代码描述了):
设一个4x2
的二维数组(不理解二维数组的话用两个一维也是一样的,具体方法放在后面说),第一维度有四个量,表示上下左右四个方向,第二维度有两个量一个表示x
一个表示y
,即往这个方向试探的话,x
或y
怎么变化↓
int go[4][2] = {
{
1, 0},{
-1, 0},{
0, 1},{
0, -1}};//上下左右试探四个方向
二维数组的试探方法如上,每个里面的的花括号代表一个方向。比如,我站在(x, y)
这个位置如果想要往上走,用(x + go[1][0], y + go[1][1])
就能顺利得到(x - 1, y)
。
用这样一个数组是不是太麻烦了?不是,如果你用一个数组,上下左右试探这件事情就可以用一个 j = 0~3
的for循环来实现遍历。循环内部(x + go[j][0], y + go[j][1])
g[x][y] = ' '; //标记通路
for(int j = 0; j < 4; j++) //试探四个方向
dfs(x + go[j][0], y + go[j][1]);
g[x][y] = 'o'; //回溯,,撤销操作
用两个一位数组来试探也是一样的,只是把描述x和y的位置变化分成了两个数组而不是放在第二维度而已。文末会提。
代码描述
写代码之前,再来过一遍思路:
- 判断当前这个位置能不能走
- 能走就标记,然后从这个位置开始上下左右四个方向试探
- 递归出口是走到了终点(当前位置==终点位置)
ps:这里迷宫的通路用o
表示,墙壁用x
以及表示图的二维矩阵g和一些变量我都用了全局的。不想这么写可以放在参数里面
#include<bits/stdc++.h>
using namespace std;
const int N = 505; //假设地图大小不超过这个范围
char g[N][N];
int n, tx, ty; //图的实际大小,终点坐标
int go[4][2] = {
{
1, 0},{
-1, 0},{
0, 1},{
0, -1}};//上下左右试探四个方向
void printGraph(){
//输出图
for(int i = 0; i < n; i++){
for(int j = 0; j < n; j++){
cout<<g[i][j];
}
cout<<endl;
}
}//printGraph()
void dfs(int x, int y){
//从图g的当前位置(x,y)走到终点(tx,ty)的深搜
if(x == tx && y == ty){
//走到终点
g[x][y] = ' '; //终点也标记一次通路,或者你也可以用其它特殊字符标记终点
printGraph();
return;
}
if(g[x][y] == 'o' && x >= 0 && y >= 0 && x < n && y < n){
//这个点可以走吗?
g[x][y] = ' '; //标记通路
for(int j = 0; j < 4; j++){
//试探四个方向
dfs(x + go[j][0], y + go[j][1]);
}
g[x][y] = 'o'; //回溯,能执行到这里说明这个点已经不可行或者可行解已经输出。还原这个位置的状态
}
}//dfs()
int main(){
int x, y;
cin>>n; //图的边长
cin>>x>>y; //起点
cin>>tx>>ty; //终点
for(int i = 0; i < n; i++)
for(int j = 0; j < n; j++)
cin>>g[i][j];
cout<<endl;
dfs(x, y);
return 0;
}
测试用例
8
0 0
7 7
oxxxxxxx
oooooxxx
xoxxooox
xoxxoxxo
xoxxxxxx
xoxxooox
xooooxoo
xxxxxxxo
好了就是这样了,是不是还蛮简单的?其实思路就三句话:
- 判断当前点可不可行
- 可行就修改这个点的状态,然后递归试探这个点可达的所有点
- 试探完成就回溯,还原这个点原始的状态
如果需要求路径条数,改一改递归出口的操作就可以了。深搜适合求路径数量(可行解数),而广搜适合求最短路(最优可行解),下次写广搜吧。。
两个一位数组做试探
啊,差点忘了两个一位数组做试探。就是分别用两个一位数组来表示x和y的试探方向。
int dy[4] = {
0,1,0,-1}; //控制左右移动
int dx[4] = {
-1,0,1,0}; //控制上下移动
注意用两个一位数组的时候,对应下标要匹配。即往上走的时候用的是(x + dx[j], y + dy[j])
这个j
对应的位置在两个数组中要匹配