深度优先搜索(DFS)

深度优先搜索(DFS)

英文写做 depth-first search 是搜索的手段之一。

它的实现基于一种名为栈的数据结构。

先在这里放一个介绍栈的链接

http://www.cnblogs.com/QG-whz/p/5170418.html

DFS是指从某个状态开始,不断的转移状态,直到无法转移,然后退回到前一步的状态,继续转移到其他状态,如此不断重复,直到找到最终的解。

可以拿解数独做例子,首先在某个格子里填上合适的数字,然后继续在下一个格子内填入数字,如此继续下去。如果发现某个格子无解了,就放弃前一个格子上选取的数字,改成其他可行的数字,再继续下一步的填数。直到全部的数字都填上。

可能大家还是不太能够理解,那么我先来解释一下一些基本概念。


首先是树。
树是图论中的概念,在这里,我们需要把树当成是储存信息的一种结构。树有节点和边两个要素构成。树满足如下条件,即所有节点都相互连通、且有唯一路径,那么很容易知道,一个n个节点的树必然有且仅有n-1条边。一般来说树不一定有根,但是在我们现在讨论的问题里,我们选择一个节点作为根,其他节点距离根的距离称作是此节点的深度。
所谓的DFS其实就是对一棵有根树的一种遍历方法。他所遵循的规则是按照深度优先的原则对各个节点进行访问。如下图所示,首先一路一直往深处走,直到不能再走,退一步换个节点继续往深处走,直到所有的节点都访问到。这就是所谓的深度优先遍历。

同时我在这里放一个用栈来模拟dfs工作原理的一篇文章

https://blog.csdn.net/qq_38442065/article/details/81634282

在这里插入图片描述


那么这种搜索方法如何体现在具体的问题中呢?简单来说,就是把问题中的每一个状态抽象成一个节点,节点与节点之间的状态改变即象征着节点与节点之间的边的连接。初始的状态就是我们在之前所定义的根节点。从根节点出发遍历这个问题的所有可能性,最终得到问题的解。其中,生成的整棵树一般被称为解答树。对解答树的遍历,也就是DFS用来解决问题的具体方式。


那么现在举一个具体的题目来解释一下的解题流程。

Description :

部分和问题

给定整数 a 0 a_0 a0 a 1 a_1 a1 a 2 a_2 a2、…、 a n − 1 a_{n-1} an1,判断是否可以从中选出若干数使得它们的和恰好为 k k k

Input :

1 ≤ n ≤ 20 1 \le n \le 20 1n20

− 1 0 8 ≤ a i ≤ 1 0 8 -10^8 \le a_i \le 10^8 108ai108

− 1 0 8 ≤ k ≤ 1 0 8 -10^8\le k\le10^8 108k108

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 nm的园子,雨后积起了水。八连通的积水被认为是连在一起的。请问园子里总共有多少水洼?(八连通指的是下图中相对w的*的部分)
***
*w*
***

Input:

n , m ≤ 100 n,m\le100 nm100
以及园子的图

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(nm8)=O(nm)

#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的一份链接。

https://blog.csdn.net/qq_33883389/article/details/78996307


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值