深度优先搜索(DFS)
英文写做 depth-first search 是搜索的手段之一。
它的实现基于一种名为栈的数据结构。
先在这里放一个介绍栈的链接
DFS是指从某个状态开始,不断的转移状态,直到无法转移,然后退回到前一步的状态,继续转移到其他状态,如此不断重复,直到找到最终的解。
可以拿解数独做例子,首先在某个格子里填上合适的数字,然后继续在下一个格子内填入数字,如此继续下去。如果发现某个格子无解了,就放弃前一个格子上选取的数字,改成其他可行的数字,再继续下一步的填数。直到全部的数字都填上。
可能大家还是不太能够理解,那么我先来解释一下一些基本概念。
首先是树。
树是图论中的概念,在这里,我们需要把树当成是储存信息的一种结构。树有节点和边两个要素构成。树满足如下条件,即所有节点都相互连通、且有唯一路径,那么很容易知道,一个n个节点的树必然有且仅有n-1条边。一般来说树不一定有根,但是在我们现在讨论的问题里,我们选择一个节点作为根,其他节点距离根的距离称作是此节点的深度。
所谓的DFS其实就是对一棵有根树的一种遍历方法。他所遵循的规则是按照深度优先的原则对各个节点进行访问。如下图所示,首先一路一直往深处走,直到不能再走,退一步换个节点继续往深处走,直到所有的节点都访问到。这就是所谓的深度优先遍历。
同时我在这里放一个用栈来模拟dfs工作原理的一篇文章
那么这种搜索方法如何体现在具体的问题中呢?简单来说,就是把问题中的每一个状态抽象成一个节点,节点与节点之间的状态改变即象征着节点与节点之间的边的连接。初始的状态就是我们在之前所定义的根节点。从根节点出发遍历这个问题的所有可能性,最终得到问题的解。其中,生成的整棵树一般被称为解答树。对解答树的遍历,也就是DFS用来解决问题的具体方式。
那么现在举一个具体的题目来解释一下的解题流程。
Description :
部分和问题
给定整数 a 0 a_0 a0、 a 1 a_1 a1、 a 2 a_2 a2、…、 a n − 1 a_{n-1} an−1,判断是否可以从中选出若干数使得它们的和恰好为 k k k
Input :
1 ≤ n ≤ 20 1 \le n \le 20 1≤n≤20
− 1 0 8 ≤ a i ≤ 1 0 8 -10^8 \le a_i \le 10^8 −108≤ai≤108
− 1 0 8 ≤ k ≤ 1 0 8 -10^8\le k\le10^8 −108≤k≤108
Output :
输出“Yes”或者“No”
样例1:
输入
n=4
a={1,2,4,7}
k=13
输出
Yes (13=2+4+7)
样例2:
输入
n=4
a={1,2,4,7}
k=15
输出
No
从
a
0
a_0
a0开始按顺序决定每个数加或者不加,自全部n个数字都决定以后再判断它们的和是不是k即可。因为状态数是
2
(
n
+
1
)
2^{(n+1)}
2(n+1),所以算法的复杂度是
O
(
2
n
)
O(2^n)
O(2n)。根据深度优先搜索的特点,采用递归函数实现比较简单。如何实现这个搜索呢,请参见下面的代码。
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=25;
int n,a[N],k;
bool is_ok;
void Input(){//输入,字母代表的含义与题目相同
scanf("%d",&n);
for(int i=0;i<n;i++){
scanf("%d",&a[i]);
}
scanf("%d",&k);
}
void dfs(int cur,int sum){//cur表示当前枚举到的数的下标,sum表示当前的和
if(cur==n){//已经枚举了所有的数字(递归边界,十分重要)
//此时已经无需再继续向下搜索,相当于到了深度最深的节点,要开始往回走了
if(sum==k){//如果此时的状态是满足条件的
is_ok=true;//标记一下
}
return; //注意一定不能忘了,回到上一个节点的状态
}
dfs(cur+1,sum+a[cur]);//选择加上a[cur],到下一个状态
dfs(cur+1,sum);//不加上a[cur],到另一种状态
}
int main(){
Input();
is_ok=false;//先标记为false
dfs(0,0);//这是初始状态,也就是根节点的状态
//根节点所在的状态,枚举到下标0,此时和为0
//如果a[0]被选中,那么将会变成dfs(1,a[0])这个状态
//如果a[0]没有被选中,那么将会变成dfs(1,0)这个状态(未被选中,所以和还是为0)
if(is_ok)puts("Yes");
else puts("No");
return 0;
}
深度优先搜索是从最开始的状态出发,遍历所有可以到达的状态。由此可以对所有的状态进行操作,或者列举出所有状态。
另外还有一种题型也比较常见下面单独指出。
Description:
Lake Counting(POJ2386)
有一个大小为
n
∗
m
n*m
n∗m的园子,雨后积起了水。八连通的积水被认为是连在一起的。请问园子里总共有多少水洼?(八连通指的是下图中相对w的*的部分)
***
*w*
***
Input:
n
,
m
≤
100
n,m\le100
n,m≤100
以及园子的图
Output:
输出一个整数表示水洼数
样例
输入
n=10 m=12
园子如下图 “w"表示积水”."表示没有积水
输出
3
从任意的"w"开始,不停地把邻接的部分用".“代替。一次DFS以后与初始的这个"w"连接的所有"w"就都被替换成了”.",因此直到图中不在出现"w"为止,总共进行DFS的次数就是答案了。8个方向共对应8种状态转移,每个格子作为DFS的参数最多被调用一次 所以复杂度为
O
(
n
∗
m
∗
8
)
=
O
(
n
∗
m
)
O(n*m*8)=O(n*m)
O(n∗m∗8)=O(n∗m)。
#include<stdio.h>
#include<string.h>
#include<iostream>
#include<algorithm>
using namespace std;
const int N=105;
int n,m;
char str[N][N];
void Input(){//输入
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%s",str[i]+1);
}
}
void dfs(int x,int y){
str[x][y]='.';//将已经访问过的点标记掉
for(int i=-1;i<=1;i++){//枚举3*3的位置范围内是否有“w”
for(int j=-1;j<=1;j++){
if(i==0&&j==0)continue;//处于原地,不用枚举
if(x+i<=n&&x+i>0&&y+j<=m&&y+j>0){//走了这一步仍然在地图中,保证不会走到地图外面
if(str[x+i][y+j]=='w')//如果周围存在“w”,那么这个“w”与当前的节点是属于同一片水洼,所以要和当前“w”一起给标记掉
dfs(x+i,y+j); //标记下一个点,并且沿着下一个点继续找
}
}
}
}
int main(){
Input();
int ans=0;
for(int i=1;i<=n;i++){
for(int j=1;j<=m;j++){
if(str[i][j]=='w'){//找一个“w”
dfs(i,j);//经过这次dfs以后,所有和这个节点相邻的“w”将会全都被标记成“.”
ans++;//这些“w”对最终答案贡献1
}
}
}
printf("%d\n",ans);
return 0;
}
补充:
DFS其实上是用了栈(stack)这种数据结构。栈是支持push和pop两种操作的数据结构。push是在顶端放入一组数据的操作,pop是从其顶端取出一组数据的操作。因此最后一组进入栈的数据将第一个被取出。这种行为叫做LIFO:last in first out,即后进先出。
递归其实上就是一个栈的结构。我们一般使用递归来DFS,因为这样子写起来十分方便。但是有兴趣的同学可以使用栈来模拟一下DFS,这对我们理解DFS本身有很大好处,同时也可以为后面学习BFS做铺垫。
这里附上采用非递归形式的dfs的一份链接。