数据结构与算法——DFS(深度优先搜索)

news2024/11/24 14:57:23

算法介绍:

深度优先搜索(Depth-First Search,简称DFS)是一种用于遍历或搜索树或图的算法。这种算法会尽可能深地搜索图的分支,直到找到目标节点或达到叶节点(没有子节点的节点),然后回溯到上一个分支继续搜索。DFS可以用于许多问题,比如路径寻找、连通性验证、拓扑排序等。

在ACM、蓝桥杯等著名竞赛中DFS算法是比较重要的,特别是在蓝桥杯中每一年几乎都要考DFS/BFS算法。DFS算法在OI赛中用处非常大,可以通过DFS/BFS暴力的方式可以拿到部分分数,蓝桥杯一般可以拿到20%的分数,有的甚至高达50%,是暴力得分的不二之选。

基本步骤:

DFS通常使用递归或栈来实现。以下是DFS的基本步骤:

  1. 选择起始点:选择图中的一个点作为起始点。
  2. 访问节点:标记起始节点为已访问,并将该节点加入递归或栈中。
  3. 探索邻接节点:从该点周围取出一个点,检查它的所有未访问的邻接节点。
  4. 递归或迭代:对每个未访问的邻接节点,将其标记为已访问,然后将其推入递归或栈中。
  5. 回溯:当当前节点的所有邻接节点都被访问后,递归中回溯/从栈中弹出该节点,继续搜索上一个点的其他分支。
  6. 结束条件:当栈为空或找到目标节点时,搜索结束。

图解算法:

下面放一张我们学校ACM在大一培训时使用的一张动态BFS/DFS步骤图。注:红色遍历为BFS、黄色遍历为DFS。(绿色为起点,紫色为终点,黑色为障碍物)

由上图中我们可以看出,DFS的遍历为一条路,用我们学长的话说就是一条路走到头,找不到就再回头换另一条路,继续搜索,直到找到终点。那么BFS就是在一个点的周围每一个点都走一遍试一下,属于扩散型的。找不到的话在这个点的基础上再去扩散四周寻找。BFS一般通过栈来实现,DFS一般通过递归来实现。

下面我们将以3*3的网格,只考虑上下左右四个方向,上左、下左等四个方向不考虑,递归顺序为{上、下、左、右},顺序可按照自己的想法,这里以上下左右顺序为例,给大家模拟实现一下过程。

第一步:

本身就在起点,先把此时起点标记为已经走过了,即vis[起点]=true,告诉后面这个点不能再搜索了,不标记的话可能会陷入死循环。在寻找起点的上下左右格子,我们发现,{上}、{左}方向都已经超出了格子范围,在题中就是超出了下标范围,那么我们能考虑的就只有{下}、{右}方向,由于我们递归的顺序为{上、下、左、右},{上}方向超出了格子范围,那么级别最高且合法的就是{下}方向了,从起点向下走。

第二步: 

先标记,vis[第一步]=true。此时我们发现{左}方向已经不在网格里面了,下标已经越界了,所以不考虑{左}方向,此时递归顺序为{上、下、右}方向,向上走,在第一步一开始我们就把vis[起点]标记为true,说明已经走过了,下面不允许走了,{上}方向也不能走了。此时优先级最高的为下方向了,下方向没有被标记过,所以可以走。

第三步: 

还是先标记,把当前位置所走的标记为已经走过,即vis[第二步]=true。此时我们发现{左、下}两个方向已经越界了,所以不考虑,此时递归优先级为{上、右},由于第三步刚开始就把vis[第二步]=true,已经标记了,不能走了,所以这一步只能向右走。

第四步:

还还还是先标记,vis[第三步]=true。此时我们发现{下}方向已经越界了,所以不考虑{下}方向了,在这一步刚开始我们把{左}方向这个点已经给标记过了,所以不能走,那么剩余的合法递归顺序为{上、右},此时我们便可以向上走,找到终点,完成此次DFS(深度优先搜索)。

 


算法模板:

从上面的图解算法中步骤我们把算法步骤归结为一下:

首先我们需要一个数组即输入的数据数组,可能为int、也肯为char,在绝大部分题目中都需要一个标记数组,即bool类型的vis数组。然后在题目中找到搜索的起点跟终点,设置dfs函数的参数,根据题目的不同参数个数不同。在主函数dfs函数之前先把vis[起点]标记为true,一定不要忘记,否则就是死循环。确定了起点,把参数传给dfs函数参数,进入dfs递归函数,确定函数的终止条件,即当前状态==目标状态或者递归值==某个数等,终止条件一定在函数的最前面,否则会影响答案。再次,在当前点,寻找当前点的周围(根据题目,可能四个点,也可能八个点等),用for循环遍历每一个点,如果这个点已经被标记过了或者数组下标越界都是不合法的,直接continue掉。那么剩下的就是合法的,可以访问下一个点,先把下一个点给标记完再去递归下一个点,再进入dfs函数递归,注意大部分dfs都需要回溯的,即把vis[当前点]=false,为什么需要回溯,这就是我们所说的一条路走不通,回头换一条路走,那么我要回头,前面走过的已经被标记的,还咋走,那就要vis[当前点]=false解除标记。下面贴一个dfs函数的一般模板。

void dfs(int dep){//dep为当前状态
	if(dep==x){//当前状态==目标状态,终止条件
		更新结果
		return;
	}
	for(int i=0;i<n;i++){//遍历每一种可能
		if(不符合条件){
			continue;//跳过
		}
		//符合条件
		vis[访问的点]=true;//先标记
		dfs(下一个状态);
		vis[访问的点]=false;//恢复现场,大部分需要回溯,有的不需要这一步
	}
}


算法例题:

现在各大算法刷题网站上dfs题目非常的多,dfs题目的变化也比较多,现在各种各样的题目层出不穷,博主所做过的题印象中大部分在终止条件哪里变化的比较多,像是还有在条件判断上增加附加条件等等,下面博主选取几个比较具有代表性的给大家讲解一下,加深理解一下dfs算法。


一、马走日

题目描述:

马在中国象棋以日字形规则移动。请编写一段程序,给定n×m大小的棋盘,以及马的初始位置(x,y),要求不能重复经过棋盘上的同一个点,计算马可以有多少途径遍历棋盘上的所有点。
输入
第一行为整数T(T < 10),表示测试数据组数。
每一组测试数据包含一行,为四个整数,分别为棋盘的大小以及初始位置坐标n,m,x,y。(0≤x≤n-1,0≤y≤m-1, m < 10, n < 10)。
输出
每组测试数据包含一行,为一个整数,表示马能遍历棋盘的途径总数,0为无法遍历一次。
输入示例
1
5 4 0 0
输出示例
32

 解题思路:

这个题不同在了马走的方向跟之前走四个方向的不同他是斜着跳,与象棋上的马一样,例如(1,2)、(-2,1)等,初始状态就是起点(x,y),终点的话,除了起点每个点都可能是终点,这样不好确定终点,但是我们有一个条件,遍历完所有点,有两种方法可以确定遍历完所有点,第一种是n*m的棋盘,那么如果走完了n*m步就完成了一次,第二种设置一个标记数组,标记棋盘上每一个点都被标记完了就完成一次,很明显第一种方法比第二种好。由于统计的是途径总数,每完成一次计数就++,直到他无路可走,自动退出函数。

AC代码:
#include<iostream>
using namespace std;
int a[15][15],vis[15][15];
int n,m,sx,sy;
int ans;//途径数
int dx[]={1,1,-1,-1,2,2,-2,-2};//马在棋盘上可以跳八个方向
int dy[]={2,-2,2,-2,1,-1,1,-1};
void dfs(int x,int y,int dep){//(x,y)当前坐标,dep当前步数
	if(dep==n*m){//终止条件
		ans++;
		return;
	}
	for(int i=0;i<8;i++){//八个方向遍历
		int bx=x+dx[i];
		int by=y+dy[i];
		if(vis[bx][by]==1){//已经被标记过了
			continue;
		}
		if(bx<0||bx>=n||by<0||by>=m){//下标越界
			continue;
		}
		vis[bx][by]=1;//先标记该点
		dfs(bx,by,dep+1);//再去递归下一次
		vis[bx][by]=0;//回溯--解标记
	}
}
int main(){
	int t;
	cin>>t;
	while(t--){
		ans=0;
		scanf("%d%d%d%d",&n,&m,&sx,&sy);
		vis[sx][sy]=1;//起点标记
		dfs(sx,sy,1);
		printf("%d",ans);
	}
	
	return 0;
}

二、AcWing 3428. 放苹果

把 M个同样的苹果放在 N个同样的盘子里,允许有的盘子空着不放,问共有多少种不同的分法?

盘子相对顺序不同,例如 5,1,1和 1,5,1算作同一种分法。

输入格式

输入包含多组测试数据。

每组数据占一行,包含两个整数 M 和 N。

输出格式

每组数据,输出一行一个结果表示分法数量。

数据范围

1≤M,N≤10;

输入样例:

7 3

输出样例:

8
解题思路:

N与M最大只有10,dfs可以适用,M个苹果放到N个盘子里,允许盘子空着不放,可以直接dfs递归实现,我们设置两个参数,一个遍历的盘子数dep,一个遍历的苹果数sum,每一次向盘子里面放t个苹果,那么苹果数sum-t,盘子数sum+1,当超出了盘子数不符合条件或者苹果数<0则返回上一步执行。

AC代码: 
#include<iostream>
using namespace std;

int a[101]={1};//初始化为1放苹果的至少放一个
int m,n,num=0;//m:苹果,n:盘子

void dfs(int sum,int dep){
	if(sum<0){//没有苹果就没法放了,回溯
		return;
	}
	if(sum==0&&dep<=n+1){//终止条件
		num++;
		return;
	}
	for(int i=a[dep-1];i<=m;i++){//防止重复放比如{1,5,1}跟{5,1,1}是一样的放法
		a[dep]=i;//放的个数
		dfs(sum-i,dep+1);//继续搜索下一个盘子
        //没有回溯,因为这是组合问题,分的苹果数重复了属于同一种
	}
}
int main(){
    while(scanf("%d%d",&m,&n)!=EOF){
        num=0;
        dfs(m,1);
	    printf("%d\n",num);
    }
	return 0;
}

三、八皇后问题 

会下国际象棋的人都很清楚:皇后可以在横、竖、斜线上不限步数地吃掉其他棋子。

如何将 8 个皇后放在棋盘上(有 8×8 个方格),使它们谁也不能被吃掉!

这就是著名的八皇后问题。

已经知道 8 皇后问题一共有 92 组解。

要求打印每一种解。

解题思路:

对于八皇后问题以后博主感觉不会出类似的题,但是我还是想把八皇后加入到例题来讲解,因为它实在是太经典了。八皇后问题是最经典的递归问题,你可以说没学过八皇后,但是不能说学了dfs但是不会八皇后。为什么说八皇后问题最经典,八皇后问题的条件一般是不会改变的,它在标记这一块动了手脚,本来由标记一个点变为标记整行、整列、点所在的主对角线、点所在的副对角线。主要麻烦在了标记这一块,标记处理好了,回溯的时候也比较好处理,现在对于标记网上有很多方法,我看了很多方法找出了一种我认为比较好的方法,是acking老师讲解的方法,跟大家分享一下。

首先,它需要标记四条线,分别是行、列、上左到右下、上右到左下,把它简化一下,我每次遍历从第一行开始,每次向下一个,这样保证了每次行不一样,可以少标记一个数组。列的话,一个一维vis数组就够了,上左到右下这条线,把它顺时针旋转45°,会发现是竖直的线。每一条线满足i-j+n不一样(i,j)是坐标。上右到左下这条线逆时针旋转45°,也会发现是一条竖线,每一条线满足i+j是不一样的。这样可以把它们的值当作下标值处理。

视频讲解:acking老师讲解--->点击这里<---

代码实现:
#include<stdio.h>
int a[10][10];
int vis1[8]/*标记列*/, vis2[16],/*标记左上右下*/ vis3[16]/*标记右上左下*/;
int n = 8, ans = 0;
void vis(int i, int j, int flag) {
	a[i][j] = flag;
	vis1[j] = flag;
	vis2[i - j + n] = flag;
	vis3[i + j] = flag;
}
void dfs(int i) {
	if (i == n) {
		printf("No.%d\n", ++ans);
		for (int i = 0; i < n; i++) {
			for (int j = 0; j < n; j++) {
				printf("%d ", a[i][j]);
			}
			printf("\n");
		}
		return;
	}
	for (int j = 0; j < n; j++) {
		if (vis1[j] == 0 && vis2[i - j + n] == 0 && vis3[i + j] == 0) {
			vis(i, j, 1);
			dfs(i + 1);
			vis(i, j, 0);
		}
	}
}
int main() {
	dfs(0);
	return 0;
}

 四、第十四届蓝桥杯省赛大学B组 岛屿个数

题目描述:

小蓝得到了一副大小为 M×N 的格子地图,可以将其视作一个只包含字符 0(代表海水)和 1(代表陆地)的二维数组,地图之外可以视作全部是海水,每个岛屿由在上/下/左/右四个方向上相邻的 1 相连接而形成。

在岛屿 A 所占据的格子中,如果可以从中选出 k 个不同的格子,使得他们的坐标能够组成一个这样的排列:(x0,y0),(x1,y1),...,(xk−1,yk−1),其中 (x(i+1)%k,y(i+1)%k) 是由 (xi,yi) 通过上/下/左/右移动一次得来的 (0≤i≤k−1),此时这 k 个格子就构成了一个 “环”。

如果另一个岛屿 B 所占据的格子全部位于这个 “环” 内部,此时我们将岛屿 B 视作是岛屿 A 的子岛屿。若 B 是 A 的子岛屿,C 又是 B 的子岛屿,那 C 也是 A 的子岛屿。

请问这个地图上共有多少个岛屿?在进行统计时不需要统计子岛屿的数目。

输入格式:

第一行一个整数 T,表示有 T 组测试数据。

接下来输入 T 组数据。

对于每组数据,第一行包含两个用空格分隔的整数 M、N 表示地图大小;接下来输入 M行,每行包含 N 个字符,字符只可能是 0 或 1。

输出格式:

对于每组数据,输出一行,包含一个整数表示答案。

数据范围:

对于 30% 的评测用例,1≤M,N≤10。
对于 100% 的评测用例,1≤T≤10,1≤M,N≤50。

解题思路: 

这个题博主感觉是蓝桥杯出的比较成功的题,它考查的非常具有思维性,打破了传统的dfs思路。加入了新的判断条件,很具有挑战性,博主感觉以后的像蓝桥杯这种比赛,dfs的考察一定会向着这个方向发展,传统的dfs将会退出题海战术。具体的讲解跟AC代码,请看我这一篇文章,文章单独讲解了这一道题,由于文章长度限制这里不再详解,请移步下面链接。

第十四届省赛大学B组(C/C++)岛屿个数-CSDN博客文章浏览阅读1.1k次,点赞30次,收藏14次。这不是普通的DFS/BFS搜索题,看着很像最少连通块,但是题目中又有了新的定义就是在陆地环里面(被陆地包围)也算属于此外围岛屿,那么我们就也要判定这种环岛屿,博主的思路是先BFS也可DFS找出连通块的个数(四个方向),建一个vector把连通块的起点存进去,方便去找环岛屿,只要有一个起点(或者此连通块任意一个点),此连通块的点便可通过移动一网打尽,再BFS(或者DFS)判定该岛屿是否属于这种环岛屿,不属于就结果加一,属于就不用加。对于 100% 的评测用例,1≤T≤10,1≤M,N≤50。https://blog.csdn.net/m0_73633807/article/details/137248445?spm=1001.2014.3001.5501


感谢大家支持,下篇更新BFS(广度优先搜索)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2045922.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue3 reactive 响应式原理源码实现

学习小满的视频&#xff0c;更详细的讲解 Vue3响应式原理 视频 需要了解Proxy、Reflect函数 目录结构&#xff1a; 配置环境&#xff1a; package.json {"name": "vue-reactive","version": "1.0.0","description": &quo…

【Kubernetes】Service 类型

Service 类型 1.NodePort2.ClusterlP3.LoadBalance4.ExternalName 在《Service 概念与实战》一文中&#xff0c;Service 的发布使用的是 NodePort 类型。除此之外&#xff0c;Service 的发布还支持 ClusterlP、LoadBalancer 和 ExternalName 这 3 种类型。 1.NodePort 在把 Se…

基于STM32开发的智能门铃系统

目录 引言环境准备工作 硬件准备软件安装与配置系统设计 系统架构硬件连接代码实现 系统初始化按钮与蜂鸣器控制显示与远程通知Wi-Fi通信应用场景 家庭智能门铃办公室访客通知常见问题及解决方案 常见问题解决方案结论 1. 引言 智能门铃系统通过集成按钮、蜂鸣器、显示屏、W…

HTML补充——表格表单

一、表格 1、在现实生活中&#xff0c;我们经常需要使用表格来表示一些格式化数据&#xff1a;课程表、人名表、成绩单 同样在网页中我们也需要使用表格&#xff0c;我们通过table标签创建表格。 2、在table标签中使用tr表示表格中的一行&#xff0c;有几个tr就有几行&#xff…

prometheus数据如何清理

1. 停止prometheus服务 2. 进到prometheus数据目录 3. 删除数据 3.1 删除持久化的数据块 Prometheus 将数据分块存储&#xff0c;每个块对应一个时间段。你可以通过查看目录中的时间戳来找到需要删除的数据块。 每个块的目录名是一个时间戳范围&#xff0c;例如 16094592000…

单片机在线升级架构(bootloader+app)

1、架构&#xff08;bootloaderapp) 在一定的时间内如果没有程序需要更新则自动跳转到app地址执行用户程序 内部flash 512K bootloader 跑裸机 48k 主要实现USB升级和eeprom标志位升级 app 跑freeRtos 464K 程序的基本功能&#xff0c;升级时软件复位开始执行bootloader升级…

互斥锁以及进程间通信

写线程 ---写资源 可以写数据 的条件 1.开始时 &#xff0c;buf空的 2.读线程 读完了 读线程 //buf充当读资源 //1.一开始&#xff0c;buf中没有数据可读的 1.写线程结束 信号量的机制 1.信号量 ----来描述 可使用的资源的个数 2.p操作 表示 使用这个资源 资…

毕业生实习与就业管理系统的设计与实现

TOC springboot297毕业生实习与就业管理系统的设计与实现 绪论 1.1 研究背景 现在大家正处于互联网加的时代&#xff0c;这个时代它就是一个信息内容无比丰富&#xff0c;信息处理与管理变得越加高效的网络化的时代&#xff0c;这个时代让大家的生活不仅变得更加地便利化&a…

保存数据至后台表

保存数据至后台表-供大数据平台使用-JOB程序 *&---------------------------------------------------------------------* *&程序名称 &#xff1a;ZBD_JOB_001 *&程序描述 : 保存数据至后台表-供大数据平台使用-JOB程序 *…

[Linux] 什么是 Shell?

一、什么是 shell ? shell在英语中的意思就是外壳&#xff0c;所以我们习惯称shell程序为壳程序。那为什么又会被叫做壳程序呢&#xff1f;那是因为shell程序是在内核上面的&#xff0c;属于操作系统的外壳部分&#xff0c;因此我们就称之为壳程序(shell)。 在 Linux 中&#…

增材制造正在加速赋能模具产业转型升级

模具&#xff0c;作为制造业的基石&#xff0c;正随着经济的蓬勃发展与产业的深度转型而面临更高要求。特别是注塑模具的冷却系统&#xff0c;传统工艺在面对随形冷却水路时显得力不从心&#xff0c;导致冷却效率无法进一步提升。而3D打印技术的崛起&#xff0c;则为模具领域开…

财务会计与管理会计(七)

文章目录 电商快递费用计算IF、VLOOKUP函数的应用 交费分布统计表SUMPRODUCT函数的应用 考勤签到统计系统OFFSET、MATCH函数的应用 出入库余额自动核算系统SUMPRODUCT、LOOKUP函数的应用 分段收费的典型案例VLOOKUP函数、MIN函数、MAX函数的应用 全额累进与超额累进计算提成全额…

《走走停停》,观后感

他这辈子看起来&#xff0c;好像就不是很成功。但是我们都很怀念这个人。 我们的文化太过强调永远&#xff0c;并把“永远”和“成功”牢牢捆绑了起来。 比如你开了一家咖啡店&#xff0c;这家店给你带来了很多快乐。但后来成本变高了&#xff0c;经营压力也变大了&#xff0…

求个位数(c语言)

1./描述 //给你一个数&#xff0c;让他进行巴啦啦能量&#xff0c;沙鲁沙鲁&#xff0c;小魔仙大变身&#xff0c;如果进行变身的数不满足条件的话&#xff0c;就继续让他变身。。。直到满足条件为止。 //巴啦啦能量&#xff0c;沙鲁沙鲁&#xff0c;小魔仙大变身&#xff1a;对…

2024/8/15 英语每日一段

A new Google update will make it simpler to request the removal of fake explicit images, as public figures, teachers and ordinary people increasingly contend with targeted abuse in the form of “deepfakes,” or realistic-looking images made with AI. While …

C# 学习笔记17:上位机助手_页面生成多控件滚动效果_保存与加载控件文本到文件_多字符串发送界面

今日继续完善更新我的上位机助手&#xff0c;这次完善多字符串发送的部分&#xff1a; 目前上位机助手支持以下功能&#xff1a; 1、 普通的16进制\ASCLL显示收发 2、 全页更新HEX显示&#xff08;会自动断串口&#xff09; 3、 日志辅助显示报错 4、 必要的清除日志区、接…

Hbase图形化界面

分享一个好用的hbase图形化界面 安装包&#xff1a;链接: https://pan.baidu.com/s/11Y2cDlme-P2xe--pYqy6MQ?pwdguag 提取码: guag 1、上传项目到linux 2、修改数据库配置信息 application-druid.yml 修改url、username、password为数据库连接信息 3、创建数据库(注意字符集…

display:flex布局,最简单的案例

1. 左右贴边 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>Title</title><style>#parent{width: 800px;background: red;height: 200px;display: flex;justify-content: space-between…

vscode 远程免密登录

Windows R 输入 cmd在命令行终端中输入 ssh-keygen 一直回车、确定 生成秘钥 3. C:\用户\xxx.ssh 拷贝公钥内容 id_rsa.pub 4. 在虚拟机~/.ssh/ 下创建文件touch authorized_keys,拷贝公钥内容 id_rsa.pub粘贴到authorized_keys里即可。

某市-2024【网安·理论】初赛-web1-扫雷-wp

进来是个简单的扫雷 看源码是纯js写的 看了下主要格子之类的生成逻辑在jms.js里 其中flag的输出条件也包含在jms.js 格式化了一下 看特征是base64了&#xff0c;然后又经过了别的操作&#xff0c;不过他混淆了一下就懒得看了。 知道的是每过一个难度的都可以拿到1/3个fl…