深搜(DFS)

深度优先搜索——不撞南墙不回头

深搜是作为一种遍历或搜索图和树的算法。

思想:不撞南墙不回头!

首先选取一个未访问的点作为源节点。从源节点选取一条路一直往下走,沿着当前顶点的边,访问这条路线,直到走不下去为止。这时返回上一顶点,继续试探访问此顶点的其余子顶点,直到当前顶点的子顶点都被访问过,那么,返回上一顶点,继续重复。从而实现遍历。

来个简单的例子模拟DFS的过程:

(首先,我们默认优先选择靠左或靠下的元素进行访问 )
在这里插入图片描述
选择A作为源节点------>(继续下一层深搜)访问到了B------->(继续下一层深搜)访问到了D------>(继续下一次深搜)搜索到了C------>(此时发现C的所有下一节点都已经被访问过了)返回C的上以节点D(因为是从D访问到C来的,所以不要走回到C的其他上节点)------>我们再遍历D的其他子节点E------->(发现E没有子节点)返回上一节点D------>(发现D没有未被访问的子节点)返回B------>(B没有被访问的子节点)返回A------>(A没有未被访问的子节点)DFS完毕
因此,此图的深搜遍历顺序:A—>B—>D—>C—>E

总结深搜的思路:沿着某条路径遍历,直到末端,然后回溯,再遍历另一条路径(走没有走过的岔路口)做同样的遍历,直到所有的节点都被访问,即回溯到源节点并且源节点已无未被访问的子节点。

举个例子:数的全排列问题,输入一个数n,输出1-n的全排列。
做如下分析:

我们把问题抽象:也就是说,有1-n个数字,要分别放到固定一列的n个盒子里,问分别有哪些不同的放法。
在这里插入图片描述

采用深搜的思想(不撞南墙不回头):
首先我们默认优先放置最小的小数字-----这步相当于选择先走靠左或靠下的路。
开始放置:1放在第一个,2放在第二个···n放在第n个,这样我们已经走到了头,所有的盒子我们都放过数字了。回溯一步并把第n个盒子里的数字n取出,我们当前处于第n-1个盒子处。当前盒子放的是n-1这个数字,我们把它取出来。这时,我们手里有n-1和n两个数字。采用最小数放置原则,但是数字n-1当前刚从这个盒子里取出来。为了避免重复,就不能再放进去了。所以,将目前未放置的数中除了n-1之外的最小数n放入第n-1个盒子里面,再往下走,走到了第n个盒子,手里剩下了一个数字n-1,自然放入第n个了。
此时我们已经拥有了两种排列:
1,2,3,···,n-1,n 和 1,2,3,···,n,n-1
继续回溯:当前在第n个盒子,回溯走到了第n-1个盒子(并取出了第n个盒子里的数字n-1),我们发现这两数字都在第n-1盒子放过,那就只能再回溯了(顺便拿起第n-1个盒子的数字n),走到了第n-2个盒子上,取出这个盒子的数n-2。当前,我们的状态时,在第n-2盒子上,手里有数字,n-2,n-2,n;那么遵循放小原则,并且不能重复,把n-1这个数字放入n-2这个盒子中,继续往下走,先把n-2放入第n-1盒子里,故第n个盒子放数字n了。
此时又出现了一种排列:
1,2,3,···,n-1,n-2,n;
再次从第n盒子回溯到n-1盒子:同理产生了 1,2,3,···,n-1,n,n-2 排列
因为不能往下走了,只能再次回溯到第n-2个盒子,放入n,········
不断重复如上操作,终会回溯到第一个盒子,第一个盒子把最大的数字n都放完之后,再次回溯到第一个盒子,第一个盒子已经无法放别的数字了。那便是全排列结束。

附上此题完整代码:

#include <stdio.h>
int a[101];//盒子,目前只能放最多100个盒子,你可以自己加,但是要同时修改book的个数 
int book[101]={0};//标记数组,标记是否已经放入 
void dfs(int step,int n)//当前在第几个盒子上,总共有多少数 
{
	int i;
	if(step==n+1){//如果站在第n+1个盒子面前了,那说明,已经放好所有的了 
		for(i=1;i<=n;i++){//输出这个组合 
			printf("%d ",a[i]);
		}
		printf("\n");
		return;//返回上一层,也就是调用它的地方 (回溯) 
	}
	for(i=1;i<=n;i++){//遵循最小原则  放入数字 
		//判断数字是否已经放入到之前的盒子
		if(book[i]==0){//数字并未放入 
			a[step]=i;//放入
			book[i]=1;//标记已经放入 
			//继续需要往下一个盒子走
			dfs(step+1,n); 
			//走完这个盒子之后的所有操作,已经又回溯到这个盒子了,马上就要在此盒子放入下一个数了,所以我们需要将刚才的数字收回 
			book[i]=0; 
		} 
	} 
	return;
} 
int main(){
	int n;
	scanf("%d",&n);
	dfs(1,n);
	return 0;
} 

通过上述,可以总结出来深搜的基本模型:

void  dfs(int  step)//当前状态
{
	//判断边界条件
	if(xxx){
		return//返回上一步
	}
	//尝试每一种可能
	for(xxx){
		//判断是否符合条件
		if(xxx){
			//继续走下一步
			dfs(step+1);
		}
	}
	return//全部遍历做完后返回
}

进入经典----->DFS解决迷宫问题
迷宫有n行m列的单元格组成,每个单元格要么是空地,要么是障碍物,找到一条从起点到终点的最短路径。
输入迷宫地图和始末点坐标。
比如说是这样的图:
在这里插入图片描述
菱形为起始点,心形为终点,六角星为障碍物。我们的解决方法便是,从起始点开始,往下深搜,每次往进搜索一步便让步数+1,搜索到心形时记录下步数,并回溯上一步,搜索别的路径。直到搜索完。
分析得知,我们每次往进遍历时有四个方向(上下左右)。我们模仿深搜的基本模型:

void  dfs(int x,int y,int  step)//当前状态
{
	int next[4][2]={{0,1},//向右 
				{1,0},//向下 
				{0,-1},//向左 
				{-1,0}};//向上
	int tx,ty,i;
	//判断边界条件
	if(x==p&&y==q){//走到了终点 
		if(step<min){
			min=step;
		} 
		return;//返回上一步
	}
	//尝试每一种可能(枚举四种走法) 
	for(i=0;i<4;i++){
		//计算下一点坐标 
		tx=x+next[i][0];
		ty=y+next[i][1];
		//判断是否符合条件(下一点坐标是否超范围,是否是障碍物或已在路径中) 
		if(tx<1||ty<1||tx>n||ty>m){//超范围,则continue下一种走法 
			continue; 
		}
		//可以思考一下为何不把超范围和下一个判断条件放在一起呢 ??? 
		if(book[tx][ty]==0&&a[tx][ty]==0){//是否是障碍物或已在路径中,0代表通畅,1代表障碍 
			book[tx][ty]=1;//标记 
			//继续走下一步
			dfs(tx,ty,step+1);
			book[tx][ty]=0;//消除标记 
		} 
		
	}
	return;//全部遍历做完后返回
}

完整代码:

#include <stdio.h>
int a[51][52];//地图 
int book[52][52];//标记 
int n,m,p,q,min=99999999;//m,n为地图大小,p,q为终点,min表示最短距离 
void  dfs(int x,int y,int  step)//当前状态
{
	int next[4][2]={{0,1},//向右 
					{1,0},//向下 
					{0,-1},//向左 
					{-1,0}};//向上
	int tx,ty,i;
	 
	//判断边界条件
	if(x==p&&y==q){//走到了终点 
		if(step<min){
			min=step;
		} 
		return;//返回上一步
	}
	//尝试每一种可能(枚举四种走法) 
	for(i=0;i<4;i++){
		//计算下一点坐标 
		tx=x+next[i][0];
		ty=y+next[i][1];
		//判断是否符合条件(下一点坐标是否超范围,是否是障碍物或已在路径中) 
		if(tx<1||ty<1||tx>n||ty>m){//超范围,则continue下一种走法 
			continue; 
		}
		//可以思考一下为何不把超范围和下一个判断条件放在一起呢 ??? 
		if(book[tx][ty]==0&&a[tx][ty]==0){//是否是障碍物或已在路径中,0代表通畅,1代表障碍 
			book[tx][ty]=1;//标记 
			//继续走下一步
			dfs(tx,ty,step+1);
			book[tx][ty]=0;//消除标记 
		} 
		
	}
	return;//全部遍历做完后返回
}
int main(){
	int i,j,sx,sy;//i,j用于循环,sx,xy表示初始坐标
	scanf("%d %d",&n,&m);//输入地图大小
	//读入迷宫地图 ,0代表通畅,1代表障碍 
	for(i=1;i<=n;i++){
		for(j=1;j<=m;j++){
			scanf("%d",&a[i][j]);
		}
	} 
	scanf("%d %d %d %d",&sx,&sy,&p,&q);//输入起始和结束坐标
	book[sx][sy]=1;//起点已经在路径中 
	dfs(sx,sy,0);//从起点深搜 
	printf("%d",min); 
	return 0 ;
}

注意:深搜时需要注意代码中是否取消标记。

扫描二维码关注公众号,回复: 10045056 查看本文章
发布了10 篇原创文章 · 获赞 1 · 访问量 181

猜你喜欢

转载自blog.csdn.net/Huberyxiao/article/details/105017214