(深度优先遍历和宽度优先遍历)DFS和BFS(2)

目录

前言

一、图的种类

二、样题引入

1.(DFS和BFS在图中比较)图中点的层次

          2.深度优先遍历解图的问题(树的重心)。 

3.一道有意思的算法题(回味童年)

4.DFS的题目(深度还原DFS的过程)

总结



前言

  在DFS和BFS(1)中,已经讲过了深度优先遍历和宽度优先遍历的过程,接下来我们通过实列,来进一步的了解深度优先遍历和宽度优先遍历。

一、图的种类

1.有向图:图中边的方向是一定的,不能逆序走。

2.无向图:图中的边没有方向,可以逆序走。没有正负方向

3.完全图:完全图:对于顶中的每一个顶点,都与其他的点有边直接相连 无向完全图:编辑任意一个具有n个结点的无向简单图,其边数小于等于n*(n-1)/2;我们把边数恰好等于n*(n-1)/2的n个结点的无向图称为完全图。 有向...

4.稀疏图和稠密图:一般的对于一个图来说,边的数目多的就是稠密图,边的数目少的就是稀疏图。

邻接矩阵要储存图的所有节点,而邻接表只储存非零节点,所以对于稀疏图一般采用邻接表,而稠密图则用邻接矩阵比较好。

二、样题引入

测试数据:

5 10
1 5
1 1
3 2
3 5
5 4
4 5
4 3
1 2
3 1
1 5 

答案为:1

1.图中点的层次

  用这个题来感受一下邻接矩阵和邻接表

首先邻接矩阵就是用一个N*N的二维数组来储存边的信息,比如a[x][y],他的意思就是有一条x->y的边。但是这里有一个问题,二维数组它的最大容量是2万多一点,所以当我们的数据量比较大的时候(如这一题的5万),编译器就会出现以下这种提醒:

 这就是二维数组开的太大,超出了限制导致的。所以用邻接矩阵虽然能写的的出来,但交到平台上也无法通过。但是如果数据量小于2万还是可以用邻接矩阵的。

邻接矩阵+STL(队列)解题

思路:

   因为点与点之间的距离都为1,所以这一题用宽搜比较合适。过程:到达一个点,检查这个点都能到哪些点,让能到达的点都入队,然后再将这个出队,重复这个过程。切记:走到每一个点,都将这个点做上标记,以后都不在走这一个点了。

这就是宽搜的过程。

题解为:

#include <iostream>
#include <cstring>
#include <queue>
using namespace std;//用 邻接矩阵表示图
const int N=10000;
int map[N][N],hh=99999;//表示两点不存在路线
int book[N],m,n,e[N];//全局变量,初始值为0
queue<int> qq;
void bfs()//因为每两个点的距离都为1,所以找最短路径用BFS比较合适
{
    book[1]=1;//标记一下,1这一个点已经遍历过
    e[1]=0;
    qq.push(1);
    while(qq.size())//当队列为空的时候停止
    {
        int k=qq.front();
        qq.pop();
        for(int i=1;i<=n;i++)//从1号点开始尝试,到n号点尝试结束
        {
            if(map[k][i]==1&&book[i]==0)//有k->i的路线,并且之前没走过
            {
                qq.push(i);
                e[i]=e[k]+1;
                book[i]=1;
            }
        } 
    } 
}
int main()
{
     //首先用邻接表输入地形
     memset(e,-1,sizeof(e));
     int a,b;
     cin>>n>>m; 
     for(int i=1;i<=n;i++)
     for(int j=1;j<=n;j++)
     map[i][j]=hh;//初始化,让所有的点都不链接,所以点之间的距离为无穷大
     while(m--)
     {
         cin>>a>>b;//表示从a走向b
         map[a][b]=1; //距离为一
     } //这样一个邻接矩阵就构造完成
     bfs();
    cout<<"答案为:"<<e[n]<<endl;
    return 0;
} 

 

 这一题的正确写法应该是用邻接表,因为用邻接表就不必考虑数组越界的问题了。

     嗯。。。。。这一题的话就不用STL了,用数组模拟队列的形式来写。感觉数组真的是个神奇的东西,学完数据结构一定要学这用数组模拟链表,栈,队列这些东西,真的很实用!

邻接表就是将一个点能一步到达的所有点用一个链表连接起来,让所有点都重复这个过程,拿上面测试数据来举例。

 简单的说一下如何用数组来模拟链表吧。h数组相当于是头指针,它指向的是下一个元素的位置,而e数组是储存相应位置上的值,而ne数组则储存的是下标指向的下一个位置。没听懂的也没关系,可以百度搜一搜。

#include <iostream>
#include <string.h>
using namespace std;
const int N=1e5+10;
int h[N],ne[N],e[N],indx=0;//邻接表储存图 (数组模拟链表)
int qq[N],head,tail;//用数组来模拟队列 
int m,n,ht[N],book[N];//ht[]数组用来储存从k点到其他点的最短路径 
void add(int a,int b)//将 a到b的 这条路线插到邻接表当中
{
	e[indx]=b;ne[indx]=h[a];h[a]=indx++;
} 
void bfs(int k)
{
	head=0,tail=0;//对队列初始化
	book[k]=1;ht[k]=0;
	qq[tail++]=k;
	while(tail>head)
	{
		for(int i=h[qq[head]];i!=-1;i=ne[i])//因为是宽度优先遍历,让邻接表的h[qq[tail]]全部入队列 
		{
			if(book[e[i]]==0) 
			{
				ht[e[i]]=ht[qq[head]]+1;
				qq[tail++]=e[i];
				book[e[i]]=1;
			}
		}
		head++;
	} 
}
int main()
{
	int p,q;
	memset(h,-1,sizeof(h));//先让邻接表中的每一个头节点都指向-1; 
	memset(ne,-1,sizeof(ne));
	memset(ht,-1,sizeof(ht));
	cin>>n>>m;
	while(m--)
	{
		cin>>p>>q;//输入一个从p到q的路线
		add(p,q);//将该路线插入到邻接表当中 
	}
	bfs(1);
	cout<<ht[n]<<endl;
	return 0;
}

 测试数据都通过了。

 2.深度优先遍历解图的问题(树的重心)。 

测试数据:

9
1 2
1 7
1 4
2 8
2 5
4 3
3 9
4 6

  树是一个无向不连通图,而无向图又是特殊的有向图。所以这一题也能用邻接矩阵或者邻接表来储存。用DFS深度优先解题比较合适,因为深度优先有一个性质,他会以一个节点为起点一直往下搜索,当无法搜索的时候就回溯,再在距离无路可走最近的地方寻找出路,我们只需要在深度搜索的时候记录其搜索了几个点,就能知道去除某一个节点后子树各有多少节点,然后再求出其它子树节点最多有多少个,再逐个节点进行比较即可。如图为测试数据。

题解为:

#include <iostream>
#include <string.h>
#include <algorithm>
using namespace std;
const int N=1e5+10;
const int M=2*N;
int h[N],e[M],ne[M],inx=0,n;//用数组模拟点链表 
int que[N],head,tail;//数组模拟队列 
int sum=N;
bool book[N];
void add(int a,int b)
{
	e[inx]=b;ne[inx]=h[a];h[a]=inx++;//相当于前插法 
}
int dfs(int x)//删除x节点,然后算出子节点的个数 
{
	//利用深度搜索的一个特点,就是“不撞南墙不回头”,比如从1结点开始深度搜索,会把1节点
	//以后的分支都会一条路遍历一遍,这样就能够得到删除节点1 后子节点的大小,然后再进行比较即可
	 book[x]=true;//标记一下x节点,以后不再遍历这个节点
	 int ans=0,ma=1;//用来找出x这个子节点的最大值 
	 for(int i=h[x];i!=-1;i=ne[i]) 
	 {
	 	int j=e[i];
	 	if(!book[j])
	 	{
		 	int s=dfs(j);
			ans=max(ans,s);	
			ma+=s;//求出以j为根节点	子节点的个数,包含j; 
		}
	 }
	 ans=max(ans,n-ma);
	 sum=min(ans,sum);
	 return ma;
}
int main()
{
	cin>>n;
	memset(h,-1,sizeof h);
	for(int i=1;i<n;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x,y);
		add(y,x);//树相当于一个无向图图(有向图的特殊形式) ,所以可以x->y,也可y->x; 
	}
	dfs(1);
	cout<<sum<<endl;
	return 0;
}

 

测试通过。

2.一道有意思的算法题(回味童年)

 测试数据:(上图的地形小编已按字符拼接完成,当然也可以自己设置地形啦!)

13 13 3 3
*************
*@@.@@@*@@@.*
***.*@*@*@*@*
*.......*..@*
*@*.***.*@*@*
*@@.@@@.*.@@*
*@*.*@*.*.*.*
**@...@.....*
*@*.*@***.*@*
*...@*@@@.@@*
*@*.*@*@*.*@*
*@@.@@@*@.@@*
*************

其主要的思想就是DFS和BFS。(过程都在题解上写着) 

DFS代码如下(示例):

#include <iostream>
#include <string.h>
using namespace std;
//用DFS完成炸弹人这一题
//用“@”表示小人,用“*”表示墙,用“.”表示空地 
char a[20][20];
int book[20][20];
int mi,mx,my,m,n;
int Max1(int x,int y)//从(x,y)这个点往四周扩,数一数往四周能炸死几个小人 
{
    
    int i=x,j=y,k=0; 
    while(a[i][y]!='*')
    {
        if(a[i][y]=='@')
        k++;
        i++;
    }
    i=x,j=y;
    while(a[x][j]!='*')
    {
        if(a[x][j]=='@')
        k++;
        j++;
    }
    i=x,j=y;
    while(a[i][y]!='*')
    {
        if(a[i][y]=='@')
        k++;
        i--;
    }
    i=x,j=y;
    while(a[x][j]!='*')
    {
        if(a[x][j]=='@')
        k++;
        j--;
    }
    return k;
} 
void dfs(int x,int y)
{
    int ne[4][2]={{0,1},{1,0},{-1,0},{0,-1}};//可以往上下左右四个方向走
    int tx,ty,sum;
    sum=Max1(x,y);
    if(sum>mi)
    {
        mi=sum;
        mx=x;
        my=y;
    } 
    for(int i=0;i<=3;i++)
    {
        tx=x+ne[i][0];
        ty=y+ne[i][1];
        if(book[tx][ty]==0&&a[tx][ty]=='.')//这个点没有走过,并且这个点是空地,可以安放炸弹
        {
        book[tx][ty]=1;//走过的就将其标记,以后不再走这个位置 
        dfs(tx,ty);//继续寻找下一个位置
         //这里不需要像全排列一样,让book指针回溯,因为我们只需要保证每一个空地试放过一个炸弹就行 
        }
    }     
}
int main()
{
    //输入地图,可以自己设计地图,也可以用图上的那个地图
    /*
    13 13 3 3(13行13列的地图,从(3,3)点开始走)
*************
*@@.@@@*@@@.*
***.*@*@*@*@*
*.......*..@*
*@*.***.*@*@*
*@@.@@@.*.@@*
*@*.*@*.*.*.*
**@...@.....*
*@*.*@***.*@*
*...@*@@@.@@*
*@*.*@*@*.*@*
*@@.@@@*@.@@*
*************
    */ 
    int kx,ky;
    cout<<"请输入图是几行几列" <<endl;
    cin>>m>>n;
    cout<<"请输入起始坐标"<<endl;
    cin>>kx>>ky;
    for(int i=0;i<m;i++)
    cin>>a[i];
    book[kx][ky]=1;
    mx=kx;
    my=ky;
    mi=Max1(kx,ky);
    dfs(kx,ky);
    cout<<"炸弹放置在点("<<mx<<","<<my<<")处炸死的小人最多"<<endl;
    cout<<"最多炸死"<<mi<<"个"<<endl;
    return 0;
} 

BFS代码如下:

#include <iostream>
#include <string.h>
using namespace std;
char a[20][20];//用于储存地图
int book[20][20];//用来判断空地是否尝试过放置炸弹 
int Max(int x,int y)//从(x,y)这个点往四周扩,数一数往四周能炸死几个小人 
{
	
	int i=x,j=y,k=0; 
	while(a[i][y]!='*')
	{
		if(a[i][y]=='@')
		k++;
		i++;
	}
	i=x,j=y;
	while(a[x][j]!='*')
	{
		if(a[x][j]=='@')
		k++;
		j++;
	}
	i=x,j=y;
	while(a[i][y]!='*')
	{
		if(a[i][y]=='@')
		k++;
		i--;
	}
	i=x,j=y;
	while(a[x][j]!='*')
	{
		if(a[x][j]=='@')
		k++;
		j--;
	}
	return k;
} 
struct kk{
	int x;
	int y;
	int s;
}que[20*20];
int main() 
{
	int kx,ky,m,n;
	cout<<"请输入图是几行几列" <<endl;
	cin>>m>>n;
	cout<<"请输入起始坐标"<<endl;
	cin>>kx>>ky;
	cout<<"请按照规定的符号,输入地形"<<endl;
	for(int i=0;i<m;i++)
	cin>>a[i];
	int next[4][2]={{0,1},{1,0},{-1,0},{0,-1}};//可以往上下左右四个方向走
	int mx=kx,my=ky,sum=Max(kx,ky);
	//对队列初始化
	int head=0,tail=0;
	que[tail].x=kx;
	que[tail].y=ky;
	que[tail++].s=0;//现在在(x,y)处,并且步数为0;
	while(head<tail)
	{
		for(int i=0;i<4;i++)//分别往四个方向扩 
		{
			kx=que[head].x+next[i][0];
			ky=que[head].y+next[i][1];
			//不需要判断 是否越界,因为 界限都为墙”*”不会出现越界的情况
			 if(book[kx][ky]==0&&a[kx][ky]=='.')//这个点没有走过,并且这个点是空地,可以安放炸弹
			 {
			 	book[kx][ky]=1;
			 	que[tail].x=kx;
			 	que[tail].y=ky;
			 	que[tail++].s=que[head].s+1;
			 	int k=Max(kx,ky);
			 	if(k>sum)
			 	{
			 		sum=k;
			 		mx=kx;
			 		my=ky;
				}
			 }
		}
		head++;//当某个点的上下左右四个点都判断完后,让头节点出队列,再 判断这个点的四周 
	} 
	cout<<"炸弹放置在点("<<mx<<","<<my<<")处炸死的小人最多"<<endl;
	cout<<"最多炸死"<<sum<<"个"<<endl;
	return 0;
}

 为什么要列举这一题呢?因为我感觉这一题挺经典的,能够反映出DFS与BFS的共同点,又能够反映出其不同点。而且这两种方法都有一些固定的模板,只要理解其过程就可以在很多题中套用。

接下来写一道只能用DFS的题目

           这也是一道比较简单的题。其思想就是DFS,我们可以模拟一下,输入的字符串多长,就相当于有几个盒子。我们将这些字母往盒子里放,因为盒子和字母的数一样多,放到最后字母刚好放完,然后再退回到倒数第二个位置,将最后两个盒子里的字母拿出来,颠倒一下位置再放置,然后再退回到倒数第三个位置,拿回后三个字母再放置。其实这就是模拟DFS的过程。

题解:

#include <iostream>
#include <string.h> 
using namespace std;
//字符的全排列
char a1[10],b1[10];
int len,book[10];
void dfs(int step)
{
	if(step==len)
	{
	for(int i=0;i<len;i++)
	cout<<b1[i];
	cout<<endl;
	return ;
	}
	for(int i=0;i<len;i++)
	{
		if(book[i]==0)
		{
			b1[step]=a1[i];
			book[i]=1;//相当于往盒子里放字母
			dfs(step+1);
			book[i]=0;//回溯的过程,也就是拿出字母的过程
		}
    }

}
int main()
{
	cin>>a1;//按照字母的升序从小到大输入
	len=strlen(a1);
	dfs(0);
	return 0;
 } 

 

还有八皇后问题,迷宫问题等,都用到了搜索。 

总结

宽搜和深搜搜索的用处很广泛,在处理一些图的问题能起到意想不到的效果。而且这两种搜索方式在算法中的位置很高,所以一定要掌握这两种方法。当然看看基础知识还是远远不够的,还需要通过大量的刷题联系。

最后,希望我的文章能够给你带来帮助。

近一星期学习算法的一个小总结!一起努力变强吧!

文章的部分题来自acwing!

  • 5
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
以下是基于的邻接表存储结构的C语言实现,使用了队列来实现宽度优先遍历: ```c #include <stdio.h> #include <stdlib.h> #define MAXV 1000 /* 最大顶点数 */ #define INF 0x3f3f3f3f /* 表示无穷大 */ /* 邻接表存储结构 */ typedef struct AdjNode { int v; /* 该边所连接的顶点编号 */ int weight; /* 该边的权重 */ struct AdjNode *next; /* 指向下一个邻接点的指针 */ } AdjNode; typedef struct Graph { AdjNode *adjList[MAXV]; /* 邻接表 */ int n; /* 顶点数 */ int e; /* 边数 */ } Graph; /* 的初始化 */ void initGraph(Graph *g, int n) { g->n = n; g->e = 0; for (int i = 0; i < n; i++) { g->adjList[i] = NULL; } } /* 添加边 */ void addEdge(Graph *g, int u, int v, int weight) { /* 添加一条从u到v的边 */ AdjNode *node = (AdjNode *)malloc(sizeof(AdjNode)); node->v = v; node->weight = weight; node->next = g->adjList[u]; g->adjList[u] = node; g->e++; } /* 宽度优先遍历 */ void bfs(Graph *g, int s) { int visited[MAXV] = {0}; /* 标记数组,记录每个顶点是否被访问过 */ int queue[MAXV], front = 0, rear = 0; /* 队列 */ AdjNode *node; /* 将起点s加入队列 */ queue[rear++] = s; visited[s] = 1; /* 遍历整个 */ while (front < rear) { int u = queue[front++]; /* 取出队首元素 */ printf("%d ", u); for (node = g->adjList[u]; node != NULL; node = node->next) { int v = node->v; if (!visited[v]) { queue[rear++] = v; /* 将v加入队列 */ visited[v] = 1; /* 标记v为已访问 */ } } } printf("\n"); } int main() { Graph g; int n, m, u, v, weight; printf("请输入的顶点数和边数:"); scanf("%d%d", &n, &m); initGraph(&g, n); printf("请输入每条边的起点、终点和权重:\n"); for (int i = 0; i < m; i++) { scanf("%d%d%d", &u, &v, &weight); addEdge(&g, u, v, weight); } printf("宽度优先遍历的结果为:"); bfs(&g, 0); return 0; } ``` 这个程序中,我们首先定义了一个邻接表的数据结构,然后定义了的初始化、添加边和宽度优先遍历等函数。在宽度优先遍历函数中,我们使用了队列来实现广度优先搜索的过程。程序的输入格式为: ``` 请输入的顶点数和边数: 6 7 请输入每条边的起点、终点和权重: 0 1 1 0 2 1 1 3 1 1 4 1 2 4 1 3 5 1 4 5 1 ``` 其中第一行输入了的顶点数n和边数m,接下来m行输入每条边的起点、终点和权重。程序的输出格式为: ``` 宽度优先遍历的结果为:0 1 2 3 4 5 ``` 这个结果表示从顶点0开始,按照宽度优先的顺序遍历时,各顶点的访问顺序。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

小魏苦练算法

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

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

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

打赏作者

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

抵扣说明:

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

余额充值