dfs与bfs的简单总结及应用 | 详解

本来想昨晚总结一下的,但是不小心玩起了游戏 ,当作放松一下了,现在我们进入正题:

一、 d f s dfs dfs的简要说明

(1):深度优先搜索(Depth-First-Search)是搜索算法的一种。是沿着树的深度遍历树的节点,尽可能深的搜索树的分支。当节 v v v的所有边都己被探寻过,搜索将回溯到发现节点 v v v的那条边的起始节点。这一过程一直进行到已发现从源节点可达的所有节点为止。如果还存在未被发现的节点,则选择其中一个作为源节点并重复以上过程,整个进程反复进行直到所有节点都被访问为止。属于盲目搜索dfs这里插入图片描述

(2): D F S DFS DFS是图论里面的一种搜索算法,他可以由一个根节点出发,遍历所有的子节点,进而把图中所有的可以构成树的集合都搜索一遍,达到全局搜索的目的。所以很多问题都可以用dfs来遍历每一种情况,从而得出最优解,但由于时间复杂度太高,我们也叫做暴力搜索

(3): D F S DFS DFS如同数据结构中的栈结构,是属于一种后进先出的结构,比如说一个羽毛球筒,把它装满之后,我们开始使用时,拿的总是最后放进去的那一个。所以这就导致了所有的点进入栈时有一个顺序,我们称之为 :DFS序

(4):根据dfs的英文全写,我们可以知道这是一种深度优先搜索的搜索算法,什么叫做深度优先?意思就是它搜索一颗子树时,它要遍历到底才会 “回头” ,比如说:上有向图中的 搜索模式 为(以DFS序来描述): a − > b − > e − > b − > f − > c − > f − > b − > c − > b − > a − > c − > a − > d − > g − > d − > a a->b->e->b->f->c->f->b->c->b->a->c->a->d->g->d->a a>b>e>b>f>c>f>b>c>b>a>c>a>d>g>d>a,有人就会问为什么搜索到 c c c的时候不搜索 a a a呢?因为我们假设的这是一个有向图。而且我们可以看到如果 你面对的图是一个 无向图,这个时候这个树将会持续搜索因为可能会形成环路,使得栈的结构一直进进出出,导致程序崩溃,所以我们也因该注意,在写DFS时,如果面对的是一个无向图的话我们需要进行标记。
一般的标记方法有:

  1. ①这个点的父节点被标记,使得子节点不能回到父节点造成循环
  2. ②访问过的节点被标记。

这两种方法视情况而定。

(5):对于暴搜来说,因其复杂度太高,就会想去优化它的复杂度,这一过程称为剪枝,剪纸分为可行性剪枝和最优化剪枝,关于剪枝引申出一种名为记忆化搜索的方法,该方法与动态规划类似。

二、 B F S BFS BFS的简要说明

(1):与 D F S DFS DFS相对的那就是 B F S BFS BFS了, B F S BFS BFS称为宽度优先搜索也叫做广度优先搜索,他是按层遍历每一种状态的下一种状态。
搞不懂?没关系我们来看一下一个例题:
给你一个整数 X X X,问是否存在一个这样一个数字 A A A

  1. ①这个数字是X的倍数
  2. ②这个数字仅由0与1这两种数字组成 例如 X=2,A=10。

这个题当然可以用 D F S DFS DFS来做,我们以 1 1 1为起点 这样就成了一个 01 0 1 01 二叉树,意思就是说每一种 情况 我们都可以 有两种选择 要么 添0,要么在后面 添1。所以我们可以写出代码:

void dfs(int start)
{
    if(A%X==0)//这是我们搜索结束的条件
    {
        ans=A;//让ans记录答案开始return
        return;
    }
    dfs(start*10);
    dfs(start*10+1);
}

这样我们就完成了搜索,起始量为1 搜先 1会变成10,11,然后10会变成100和101,11变成110与111。我们这就完成了所有的情况遍历,当A%X==0时我们记录最后的答案。

所以我们开始试例子:输入 2 2 2按照 d f s dfs dfs序,首先不符合条件,然后进入下一步搜索, X ∗ 10 = 10 X*10=10 X10=10,发现 10 % 2 = = 0 10\%2==0 10%2==0,然后开始返回ans记录A,这就是最后答案,但是你会发现程序崩溃了。

我们分析一下原因:

D F S DFS DFS为深度优先搜索,所以我们的左子树可以看作是在末尾加 0 0 0,然后右子树可以看作在末尾加 1 1 1,所以这样就形成了一棵树。按照 D F S DFS DFS我们的意图是让他搜索左子树,所以在左子树中找到了 满足条件的 数 : 10 10 10。但是为什么程序会崩溃呢?
因为搜索到树之后,按照我们刚刚的 D F S DFS DFS序,这个树遍开始回溯(你可以想象成这棵树开始回缩),每回溯到一个节点,因为这个节点的 右子树还没有搜索,所以会再去搜索这个节点的右子树,但是这个节点的右子树是在末尾进行加1,而末尾是 1 1 1绝对不可能是 2 2 2 的倍数 所以这个树会一直按照 往右子树 搜索的趋势 进行下去,导致程序崩溃。

(2):那是不是这个问题只能枚举了呢?其实 D F S DFS DFS是可以的,当搜索的深度为 19 19 19时,因为此时的数字位数已经到达了1<<19,此时的答案一定会出现,这时候回溯就好了。所以说我们加一个step记录深度就可以了:

void dfs(int start,int step)
{
    if(A%X==0&&step==19)//这是我们搜索结束的条件
    {
        ans=A;//让ans记录答案开始return
        return;
    }
    dfs(start*10,step+1);
    dfs(start*10+1,step+1);
}

(3):还有没有更好的方法呢?在考虑这个问题的时候我们回顾一下刚刚为什么程序崩溃,原因就是因为DFS是一个深度优先搜索,他每次必须要 深搜到底 ,所以对于末尾是1来说永远没有结束条件所以它一直会搜索下去!这个时候我们可不可以想有没有这样一种搜索模式,出现A=10这个情况之后我们立刻停止就可以了呢?当然是有的,那就是BFS。

(4):弄完上面这个例题,也就真正的引出了 B F S BFS BFS也发现了 D F S DFS DFS的一些小缺点。下面说一下上面留下的定义:按层遍历每一种的状态的下一种状态。首先 B F S BFS BFS是与数据结构中的 队列 相似,队列是一种 先进先出的结构,这又比如说 你把一些水果按时间一天一个放进一个箱子里,但这些水果时间长了会变质,我们假设变质日期是一样的,那么你每次拿都是拿的最早放进去的那个。
我们来看一下它的搜索模式:
BFS的访问过程与图中序号一致

在这里插入图片描述
如果我们按照 B F S BFS BFS顺序搜索,就会时上图的样子,首先把 根节点 1放进去,然后把他的 3 3 3个子节点放入队列之中,然后第一个再把第一个进入队列的子节点的三个子节点放入对列中,然后再去访问第二个放入队列的也就是上图中的3号点,所以我们看到这个过程 是一层层的搜索,把这一层所有的情况搜索一遍,又把这一层所有状态对应的下一种状态入队列,这样一步步 搜索完成。
(4):所以说对于刚刚那个题目而言,我们如果按层遍历就不会出现程序崩溃的现象,比如说 第二层有 10 10 10 11 11 11,那我们就不必搜索下一层了。因为这一层里面已经有我们想要的答案了。具体伪代码:

 1入队列
 	取出当前队列的首元素,用一个值记录,然后扔掉。
 	符合要求?跳出循环:继续状态搜索
 	把该元素对应的 两种状态入队列 (+0+1

在这里打个广告吧,介绍一下我的公众号, 刚开始做,对于对算法感兴趣的同学或者正在找工作学习的同学非常有帮助喔,会不定期更新大厂笔试内容、计算机基础知识,同时也会介绍基础算法,马上就发表dfs、bfs 最新最详细的讲解了,赶紧关注吧!
在这里插入图片描述

三、 D F S DFS DFS的应用

(1). D F S DFS DFS由于有回溯的步骤,所以我们可以对其进行更新,比如并查集合并时的状态压缩都是使用 D F S DFS DFS,还有图论中的 t a r j a n tarjan tarjan算法, l c a lca lca等等。

(2):搜索 有状态约束的 问题,比如说:
在一个给定形状的棋盘(形状可能是不规则的)上面摆放棋子,棋子没有区别。要求摆放时任意的两个棋子不能放在棋盘中的同一行或者同一列,请编程求解对于给定形状和大小的棋盘,摆放k个棋子的所有可行的摆放方案C。
Sample Input
4 4
…#
…#.
.#…
#…
Sample Output
1

题目要求:棋子只能放在#上并且 k k k个棋子必须在不同行不同列,所以说这里 进行了状态的限制,我们如果用 B F S BFS BFS做肯定不太好做,但是我们刚好可以运用 D F S DFS DFS的回溯特点做这个题目;
因为这个题目是二维的题目我们无法判断 该行该列到底有没有放棋子,我们只可以判断我们当前到的这一行放棋子了没有,于是我们在外面加一个数组 V I S VIS VIS,它标记着第i列有没有放棋子,如果第 i i i列放了棋子,那么我们就令 v i s [ i ] = = t r u e vis[i]==true vis[i]==true。然后我们 d f s dfs dfs他的每一行,在其间遍历他的每一列具体可能说不太清楚,我把代码和注释附上:

bool vis[50];
int n,k;
char mp[50][50];//标记每一列
/*AshGuang的版权*/
ll cnt=0;
void dfs(int x,int way)//用way记录我们放了多少棋子
{
    if(way==k)
    {
        cnt++;//cnt记录方案数
        return;//一定记得要return
    }
    if(x>=n) return;//这是判界 因为我们按行遍历,一共有n行不能多出去
    for(int i=0;i<n;i++)//判断这一行的每一列
    {
        if(mp[x][i]=='#'&&!vis[i])//如果说这mp[x][i]刚好是#而且 第i列没有放棋子 
        {
            vis[i]=1;//我们就放上
            dfs(x+1,way+1);//在下一行放,这一行已经无法放了,不同行不同列
            vis[i]=0;//这个地方比较关键,回溯的重点//注释1
        }
    }
    dfs(x+1,way);//这一行找不到的话就直接进行下一行//注释2
}
int main()
{
    while(~scanf("%d%d",&n,&k))
    {
        if(n==-1&&k==-1) break;
        cnt=0;
        memset(mp,0,sizeof(mp));
        for(int i=0;i<n;i++)
            scanf("%s",mp[i]);
        dfs(0,0);
        printf("%d\n",cnt);
    }
    return 0;
}

对于注释1的解释:因为我们根据题目要求 每一行每一列只能放一个棋子,那么如果这个棋子放在了当前这一行,我们就可以进入下一行搜索,那么这个搜索完成之后vis[i]=0,这个意义是什么呢?意思是这样的,当我们在这个根节点下 面所有的情况搜索完了之后,我们会回溯到这个结点,也就是说我们遍历完了 如果把 这个棋子放在 这一行这一列的 这个位置的时候,下面所有的状况,那么我们要进行 下一个状态搜索:也就是把棋子放在这一行的下一列的时候,那么这个时候 我们放的棋子绝对不是在这一列了,我们令这一列没有被标记过。
对于注释2的解释:这个有两个作用:

  1. ① 如果这一行没有棋子可放我们可以跳到下一行继续搜索。
  2. ②这一行有棋子可放我们可以遍历完这一行有棋子可放的情况,但是还有一种情况就是,这一行不放棋子,这也是可以的,因为只要到最后放够k个就足够了。

我们可以发现,要写这种 d f s dfs dfs的话注意的细节还是很多的。

(3) 搜索联通块问题

比如说题目要求 一个矩阵中 只有1与0两种数字 ,问有几块儿1,对于块儿的定义:如果1的 九宫格范围内 也有1,那么这个1与 九宫格范围内的 1是一块儿,比如说:

1 0 1
0 1 0
1 0 1
输出:1

1 0 1 0 0
0 1 0 0 0
1 0 1 0 1
输出:2

这个问题是可以用 B F S BFS BFS来写的,但是容易超时,所以用 D F S DFS DFS写最好不过了,根据 D F S DFS DFS的特点,深度优先搜索嘛。我们对一个起点开始遍历他就会把他所有能按照要求到达的点 都遍历一遍。

伪代码:
从一个起点开始:
		该起点标记为1
		遍历该起点的每一个方向,如果是1,继续进行扩展延申
		结束之后,与该起点通过一定规则可以相连的都变成了1
		遍历下一个起点,遍历之前看看这个节点是否被标记过,没有cnt++;
具体代码:
void dfs(int x,int y,int id)
{
    if(x<1||x>m||y<1||y>n) return;
    if(num[x][y]||str[x][y]=='*') return;
    num[x][y]=id;
    for(int i=0;i<8;i++)
    {
        int mx=x+dx[i],my=y+dy[i];
        dfs(mx,my,id);
    }
}
  for(int i=1;i<=m;i++)
         for(int k=1;k<=n;k++)
               if(str[i][k]==1&&num[i][k]==0) dfs(i,k,++cnt);
  printf("%lld\n",cnt);
  最后cnt的值就是联通快个数

四、 B F S BFS BFS的简要应用

(1):求最短路:

这个是图论里面的一种应用被称为SPFA算法,在我之前的博客中有总结过,所以在这里不再提。

(2):求迷宫最短路:

这种题目用DFS也是可以解的,用 DFS的思路:有许多条路可以到达终点我们求一下 到达终点的最小值。这样是非常耗时间的 而BFS不一样因为BFS是按层遍历,所以 每一层对于根节点来说距离一定是最小的,不需要再次更新,我们到达终点时这个距离一定一定是最小的,比如说:
在这里插入图片描述
还是拿这张图来说 让我们求到达的C的最小距离,按照BFS的思路,第二层就可以到C我们直接break就可以了,为什么?因为如果a->b-f->c一定不会比直接到c近,这就是BFS最特殊的性质,在搜索过程中保留了最短路。所以我们用一个辅助数组来标记记录 到当前节点的 最小距离,如果 f还要访问c时,判断一下 如果 c这个地方的距离已经被更新,说明它已经确定了最小值,后来的无法更新,我们就不让c进入队列。这样一来最小值也就确定了。
拿一个例题来说,1代表墙壁,0代表可通路每次可以向四个方向出发,起点在左上方,终点在右下方,问最短距离:

伪代码:(用num[i][j]表示第i行第j个位置走过没有)
因为初始起点需要走0步,我们把num数组 初始-1mem(num,-1);
左上角为起点:
	num[1][1]=0;
	1 1进入队列
			取出当前队列第一个元素(因为二维通常需要使用结构体)
			判断它是否为终点位置?breakcontinue;
			遍历该点四个方向可行方向
				if(num[mx][my]==-1)
					num[mx][my]=num[i][k]+1
				把这个点进入队列。
	结束
具体代码:
int bfs()
{
	queue<node>q;
	q.push({1,1});
	num[1][1]=0;
	while(!q.empty)
	{
		node u=q.front();q.pop();//扔掉首元素
		for(int i=0;i<4;i++)
		{
			int mx=u.x+dx[i],my=u.y+dy[i];
			if(u.x==ex&&u.y==ey) return num[u.x][u.y];//放队列之前我们已经把距离更新了,只要找到这个点绝对是最小距离了。
			if(num[mx][my]==-1&&str[mx][my]==0)//这里还需要加一个判界,我就不手写了有点累了。
			{
				num[mx][my]=num[u.x][u.y]+1;
				q.push({mx,my});
			}
		}
	}
}

(3):同时移动性题目问题:

火山在喷发,你在跑问是否可以跑出去。
详情之前解释过这个问题:Fire!

(4):倒水问题:

每次倒水状态之间的转移,之前也有过例题:非常可乐! P O J 341 POJ341 POJ341pots

(5):路径还原:

问从一个点到另一个点 的最短路径是多少,而题目不要求你输入最短距离,而是要求你还原路径这种时候通常需要加一个结构体数组来记录,其实上面的例题pots也是一个路径还原的问题,用一个简单的例题总结一下路径还原:
定义一个二维数组:

int maze[5][5] = {

0, 1, 0, 0, 0,

0, 1, 0, 1, 0,

0, 0, 0, 0, 0,

0, 1, 1, 1, 0,

0, 0, 0, 1, 0,

};

它表示一个迷宫,其中的1表示墙壁,0表示可以走的路,只能横着走或竖着走,不能斜着走,要求编程序找出从左上角到右下角的最短路线。
输出;
(0, 0)
(1, 0)
(2, 0)
(2, 1)
(2, 2)
(2, 3)
(2, 4)
(3, 4)
(4, 4)
我们定义结构体要加一个pre,记录它是由哪个点转移过来的:

#include <stdio.h>
#include <queue>
#include <algorithm>
#include <string.h>
using namespace std;
struct node{
    int x,y,pre;
}q[5000];
node save[5000];
int mp[50][50];
bool vis[50][50];
int my[6]={0,0,-1,1};
int mx[6]={-1,1,0,0};
int dis[50][50];
int head,cur;
int P;
int BFS()
{
    vis[1][1]=1;dis[1][1]=0;
    q[1].x=1;q[1].y=1;q[1].pre=-1;
    head=1;cur=1;
    while(head<=cur)//这是自己用数组写的队列,其实性质是一样的
    {
        int u=head;head++;
        if(q[u].x==5&&q[u].y==5)
        {
            P=u;
            return dis[q[u].x][q[u].y];
        }
        for(int i=0;i<4;i++)
        {
            int dx=q[u].x+mx[i];
            int dy=q[u].y+my[i];
            if(mp[dx][dy]||vis[dx][dy]||dx<1||dx>5||dy<1||dy>5)//判界
                continue;
            dis[dx][dy]=dis[q[u].x][q[u].y]+1;
            vis[dx][dy]=1;
            int nextN=++cur;
            q[nextN].x=dx;
            q[nextN].y=dy;
            q[nextN].pre=u;//记录这个状态转移而来的之前数组的下标。
        }
    }
}
输出时:只需要判断 对应下标的数组里面 存的是 x==1&&y==1就好了。
用回溯写:
void print(int a)
{
    if(q[a].x==1&&q[a].y==1)
    {
       printf("(%d, %d)\n",q[a].x-1,q[a].y-1);
       return;
    }
    else
    {
        print(q[a].pre);
        printf("(%d, %d)\n",q[a].x-1,q[a].y-1);
    }
}

五、总结

  1. 到这里 d f s dfs dfs b f s bfs bfs的简要介绍与总结也就写完了,如果具体还不懂可以在下方留言,欢迎及时交流。

  2. 任何有关侵犯版权问题,我都会及时道歉,谢谢宽容。

  3. BFS与dfs各自都有优点,具体问题具体分析。

  4. DFS的树的深度如果过长的话考虑用 B F S BFS BFS而不去用 D F S DFS DFS
    2020.12.25update

    	                 人一我百,人十我万!加油!码农!
    
评论 22
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只酷酷光儿( CoolGuang)

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值