bfs和dfs搜索入门

一、图

1、有向图与无向图

有向图:相当于马路的单行线,只能单向通过
无向图:相当于一条普通的马路,既能来也能去,是双向均可实现的

2、权值与网

每条边可以标记数值,代表权值的意义,可以用来求最短路问题之类的

带权的图叫做网

3、连通图

两个顶点之间有路径则称二者是连通的,若图中任意两个顶点之间都连通,则称这张图为连通图

4、建边

对于一张图最重要的是建边,首先要把边建好才能知道当前节点能够到达哪些节点

做题时顶点数目是巨大的,如果用map矩阵存图
如下:

int mp[n][n];
eg.
x->y  表示x到y有一条边
//无向边:x到y有一条边,y到x也有一条边,是要建两条边的 

mp[x][y]=1;  //代表x到y有一条边
/*
但是如果n是1e5这样的级别的话
这样的二维矩阵是开不下的
而且一一遍历时的时间复杂度过高 
*/

因此利用map建边不可取!!!

常见的建边方式有两种:

①利用vector容器
vector<int>G[n];   //如果边有权值 可以把int改为一个结构体 直接把整个结构体推进去即可 
                   //结构体中存入①该点连出去的点 ②边的权值 
void way1()  //类似于数组的形式 直接一个个推进去就好了 
{
 	G[x].push_back(y);   //加边方式 
	        //x对应遍历一遍 即从x连出去的所有点 
	int sz=G[x].size();   //获得x连出去的所有点 
	       //x连出去多少点 size就有多大 遍历sz时的时间复杂度就会降低 
	for(int i=0;i<sz;i++)
		int to=G[x][i];  //to即为x连出去的所有点 
} 
②链式前向星
int tot,ver[n*2],next[n*2],head[n];
//tot:给每个边编号  
//ver数组:存储每个编号的边连出去的点 
//next数组:该往前连接的边的编号(若没有则为0) 
//head数组:该最后一个编号的边
void add(int u,int v)  //添加一条从u->v的边 
{
	++tot;
	ver[tot]=v;
	next[tot]=head[u];
	head[u]=tot; 
}
void way2()   //链式前向星  有比较多数组 
{
	add(x,y);   //加边方式
	for(int i=head[x];i;i=next[i])  //通过遍历获取跟u相连的所有点的编号 
	{                               //终止条件是i!=0 (i=0就是没有边了)
		int to=ver[i];  //to即为x这条边所连出去的所有的点 
	}
}

int main()
{
	memset(head,0,sizeof head);  //全部置0 相当于初始化为每条边没有指向 
    /*   主函数    */ 
}

二、BFS

1、理解

遍历时可以类比于波一样的扩散方式,遍历距离一条边的,两条边的,三条边的…直到全部遍历完
(更多详情看优质blog传送门:BFS广度优先搜索

2、模板示例

  • HDU 2612为例:
Find a way
  • 题意: 给定n,m两个整数,代表这是一个n*m大小的字符矩阵,‘Y’,‘M’代表两个人,‘#’代表此路不通,不能走这个点,‘.’代表道路,可以走这个点,‘@’代表kfc,两人要到kfc汇合,上下左右都可以走,每走一步花费11min,求最短的时间总和
  • 输入: 输入包含多个测试用例。每个测试用例包括前两个整数n,m.(2<=n,m<=200)。接下来的n行,每行包含m个字符。“Y”和“M”表示两人初始位置。“#”禁止上路;’.'路。“@”KCF
  • 输出: 对于每个测试用例,输出两人到达其中一个肯德基的最短总时间。

Sample Input
4 4
Y.#@

.#…
@…M
4 4
Y.#@

.#…
@#.M
5 5
Y…@.
.#…
.#…
@…M.
#…#

Sample Output
66
88
66

方法①
#include<bits/stdc++.h>    //bfs 
using namespace std;
#define pii pair<int,int>   //pair相当于存储一对东西
//pii Point=make_pair(x,y);  //例如要将x和y进行绑定
//x= Point.first;   //获取x      y= Point.second;   //获取y   //这样就能把这一对获取完
const int N=200+10;
const int M=10;
const int inf=0x3f3f3f3f;  //inf趋近于无穷大
char s[N][N];  //代表每个点
int sx[M],sy[M];  //代表起点信息  //M=0代表‘Y’这个人,M=1代表‘M’这个人 (以下数组同理)
int n,m,dis[M][N][N];  //因为有两个人,每个人对应的dis数组不一样,所以开了三维数组
int x,y,dx[M]={0,0,1,-1},dy[M]={1,-1,0,0};  //dx和dy代表上下左右的坐标变化(***)
void bfs(int id){  //需要队列来实现  //id代表当前是哪一个人
	queue<pii>q;   //先进先出
	q.push(make_pair(sx[id],sy[id]));
	dis[id][sx[id]][sy[id]]=0;  //把这个人所在的起点置零
	while(!q.empty())
	{
		int x=q.front().first,y=q.front().second;  //获取当前队首的点
		q.pop();   //不能忘记!!! 否则会卡死在这里
		for(int i=0;i<4;i++)
		{
			int xx=x+dx[i],yy=y+dy[i];  //代表下一步要走的点  //把上下左右依次都走一遍试试
			if(1<=x&&xx<=n&&1<=yy&&yy<=m&&dis[id][xx][yy]==inf&&s[xx][yy]!='#')   //if内判断能不能走
			{  //xx和yy要在矩阵范围内  //若dis为inf说明还没走过这个点 那就可以走  //并且下一步不能是死胡同
				dis[id][xx][yy]=dis[id][x][y]+1;  //则将下一步置为出发点+1的距离
				q.push(make_pair(xx,yy));  //再将下一步的这个点推进去
			}    //以此类推,直到将整张图的点遍历完
		}        //那么这个人从起点到达任意一个点的最短距离都可以求出来了
	}
} 
int main()
{
	while(scanf("%d %d",&n,&m)!=EOF)
	{
		memset(dis,inf,sizeof dis);
		for(int i=1;i<=n;i++)  //读入矩阵
		   scanf("%s",s[i]+1);
		for(int i=1;i<=n;i++)
		{
			for(int j=1;j<=m;j++)
			{
				if(s[i][j]=='Y')  //找坐标起点  //横坐标是i 纵坐标是j
				{
					sx[0]=i;
					sy[0]=j;
				}
				if(s[i][j]=='M')
				{
					sx[1]=i;
					sy[1]=j;
				}
			}
		}
		bfs(0),bfs(1);  //因为有两个人所以要bfs两次
		int res=inf;  //result 代表总步数
		for(int i=1;i<=n;i++)  //开始遍历
		{
			for(int j=1;j<=m;j++)
			{
				if(s[i][j]=='@')
				  res=min(res,dis[0][i][j]+dis[1][i][j]);  //求出这两个人到达kfc的最短距离
			}	
		}
		printf("%d\n",res*11);   //走一步需要11min 所以最后总步数*11=总时间
	}
}

其实一开始别的都能马上理解,唯独(***)的部分突然就看不懂了,就是理解不了用dx、dy数组去标记方向变化的原理,wsr给我讲了好多还是迷迷糊糊的(呜呜呜呜我有点子笨笨),然后他把他的码发我了,结果他的码我除了表示方向变化的moven数组别的也都懂,那么问题还是同一个。经过艰难的抽象交流后,我终于有点子明白了。wsr是用二维数组moven直接记录横纵坐标,上面那个码是dx和dy分开记录了横纵坐标,但其实原理是一样的。
记录方向变化
通过数组表示的x、y的1或-1的值来表示上下左右的移动方向
这里再贴一个wsr的码,他是用了结构体去推进队列:

方法②
#include<bits/stdc++.h>  //wsr的pro a 码 
using namespace std;
#define inf 0x3f3f3f3f
const int maxn = 2e2 + 10;
int n, m, d1[maxn][maxn], vis[maxn][maxn], d2[maxn][maxn], ans;
char mp[maxn][maxn];
int moven[5][5] = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
struct Node{
	int x, y, d;
};
bool check(int x, int y) {
	if(x < 1 || x > n || y < 1 || y > m || vis[x][y] || mp[x][y] == '#') return false;
	return true;
}
void BFS(int x, int y) {
	ans++;
	memset(vis, 0, sizeof(vis));
	Node tmp;
	tmp.x = x, tmp.y = y, tmp.d = 0;
	queue<Node>q;
	q.push(tmp);
	vis[x][y] = 1;
	while(!q.empty()) {
		Node now = q.front();
		q.pop();
		for(int i = 0; i < 4; i++) {
			int mx = now.x + moven[i][0];
			int my = now.y + moven[i][1];
			if(check(mx, my)) {
				vis[mx][my] = 1;
				tmp.x = mx, tmp.y = my, tmp.d = now.d + 1;
				q.push(tmp);
				if(mp[mx][my] == '@') {
					if(ans == 1)
						d1[mx][my] += tmp.d;
					else
						d2[mx][my] += tmp.d;
				}
			}	
		}
	}
}
int main() {
	while(~scanf("%d %d", &n, &m)) {
		ans = 0;
		for(int i = 1; i <= n; i++)
			scanf("%s", mp[i] + 1);
		memset(d1, 0, sizeof(d1));
		memset(d2, 0, sizeof(d2));
		for(int i = 1; i <= n; i++) {
			for(int j = 1; j <= m; j++) {
				if(mp[i][j] == 'Y' || mp[i][j] == 'M') 
					BFS(i, j);
			}
		}
		int minn = inf;
		for(int i = 1; i <= n; i++) {
			for(int j = 1; j <= m; j++) {
				if(d1[i][j] == 0 || d2[i][j] == 0) continue;
				else{
					minn = min(d1[i][j] + d2[i][j], minn);
				}
			}
		}
		printf("%d\n", minn * 11);
	}
	system("pause");
	return 0;
}

三、DFS

1、介绍

优质blog推荐:广搜和深搜
这篇里面关于dfs的图形讲解很好理解

dfs可以用来暴搜,如果要在暴搜的基础上对其进行优化,那么就要用到剪枝,可以剪掉一些不必要的搜索,从而大大降低时间复杂度
剪枝相关blog传送门:剪枝

2、模板示例

  • HDU 2553 为例
N皇后(经典)
  • 题意: 在N*N的方格棋盘放置了N个皇后,使得它们不相互攻击(即任意2个皇后不允许处在同一排,同一列,也不允许处在与棋盘边框成45角的斜线上。对于给定的N,求出有多少种合法的放置方法。
  • 输入: 共有若干行,每行一个正整数N≤10,表示棋盘和皇后的数量;如果N=0,表示结束。
  • 输出: 共有若干行,每行一个正整数,表示对应输入行的皇后的不同放置数量。

(注: 查询若干行,可以预先处理出1-10的所有答案,再询问直接出答案)

Sample Input
1
8
5
0

Sample Output
1
92
10

#include<bits/stdc++.h>
using namespace std;
const int N =10+10;
int f[N],vis[N],n,cnt;  
bool check(int row,int col)
{   //check函数判断第row行第col列能不能放置棋子 
	if(vis[col])return false;   //vis[col]表示的是第col列放置了第几行  //eg. vis[2]=3表示第2列放置了第3行
	for(int i=1;i<=n;i++)  //按列依次判断45°的情况 
		if(vis[i] && abs(i-col)==abs(vis[i]-row)) return false;   //if判断条件后者:abs(列-列)=abs(行-行)  即为45°线上
	return true;
}
void dfs(int now)
{   //now表示当前准备放置第now行 
	if(now==n+1)   //能够放置到第n+1行说明前面的n行都成功放置了,那么说明这种方法也可以
	{    
		cnt++;
		return;
	}
	for(int i=1;i<=n;i++)
	{
		if(check(now,i))  //判断第now行第i列能不能放(算是剪枝操作)
		{                //如果可以放
			vis[i]=now;  //标记第i列放置了第now行 
			dfs(now+1);  //放置下一行 
			vis[i]=0;    //还原 当作没放过 才不会影响对下一行的重新判断
		}
	}
}
int main(){
	for(n=1;n<=10;n++)
	{
		memset(vis,0,sizeof vis);
		cnt=0;   //初始化
		dfs(1);  //每次从第一行开始放起
		f[n]=cnt;   //记录每个N值对应的种数
	}
	int x;
	while(scanf("%d",&x)!=EOF)
	{
		if(x==0)  break;
		printf("%d\n",f[x]);
	}
}

注: 1.题中45°并非只是主对角线和副对角线,而是只要与棋盘成45°的都不能放两个棋子
2.对vis数组的含义一定要理清楚,它是起到标记作用的
3.在依次判断的过程中,一定要牢记初始化,不管是种数cnt也好还是vis数组也好,都要记得初始化
4.为了降低时间复杂度,我们可以具体问题具体分析,用if判断条件来实现剪枝的操作,过滤掉不必要的搜索
5.对于n上限小的题目,可以预处理所有n取值的情况,最后直接访问数组输出对应种数,避免tle

(一些例题下次再发 先放过自己)

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值