暴力美学2——BFS广度优先搜索

最近在看《算法图解》这本书,我对算法的学习顺序也是根据这本书展开的,这本书上只写了BFS广度优先搜索,没有写BFS深度优先搜索,因此考虑到俩者的关联性,我就先去初步掌握了DFS之后再来学习BFS,有了前面的基础,BFS就相对好掌握很多了。DFS算法和BFS算法都是遍历图的算法,只是它们遍历的方式有所不同,这就会导致在实际应用中它们的应用场景也会有所不同,但其实在很多情况下俩者都是可以相互替换的。其中这篇文章中讲的DFS算法广度优先算法的主要目的是找出最短路径(但不是带权的),

举三个《算法图解》上的例子:

1.编写国际跳棋,计算最少走多少步就可获胜;
2.编写拼写检查器,计算最少编辑多少个地方就可将错拼的单词改成正确的单词,如将READED改为READER需要编辑一个地方;
3.根据你的人际关系网络找到关系最近的医生

我下面给出一张图,你就可以很清楚地看出BFS算法的基本思想了。

在这里插入图片描述
BFS算法的搜索顺序是先搜索初始节点相邻的节点,然后将这次搜索中搜索节点的相邻节点记录下来,等这一轮搜索完后(也就是初始节点相邻的节点搜索完后),再搜索已经记录下来的节点,将这个节点的相邻节点记录下来,等这一轮搜索完后,再搜索记录下来的节点。之后就是不断重复这个过程。 这其实就是队列的实现,队列的工作原理和现实生活中的队列完全相同,队列只支持入队和出队,并且是“先进先出”,“先到先走”,十分公平。BFS算法是将记录的节点直接塞到队列后面,等到前面的节点都搜索完,离开队列后,再开始搜索这些节点,从这个角度看,BFS实际上就是对应队列这个数据结构,而这个数据结构在C++中我们可以通过queue模板实现。

当然,这个算法理解起来虽然比较简单,但是想要用代码实现,还是有点难度,这边我就举两个实际的例子。

第一个例子

Joker先生最近手头没钱了,于是开始着手挖油田(后面程连块),在暴力掠夺了一大堆土地后,他需要你给他统计每一个土地中油田的数量,由于Joker先生过于贪婪,土地简直无法度量,因此人工是无法实现的,需要你通过编程让计算机小朋友去帮你统计。要求:输入一个m行n列(1<=m,n<=100)的字符矩阵,统计字符“@”组成多少个连块。如果两个字符“@”所在的格子相邻(八个方向),就说明他们属于同一个连块。多组输入,当m=0时,输入结束。

如图,就有两个连块
在这里插入图片描述
这道题目我之前将其放到了DFS算法的专题里面,实际上这道题目完全可以用BFS来做,因为在这道题目里面,只需要实现搜索的功能就可以了,因此无论是DFS还是BFS都可以实现。

由于本人实在过于懒惰,这边就将大佬的代码贴上去了,我主要是在后面加上了注释,帮助大家理解,因为没有注释去阅读代码实在过于吃力。。。

#include<bits/stdc++.h>
using namespace std;
const int maxn=105;
int m,n;
int vis[maxn][maxn];
char s[maxn][maxn];
int cnt=0;
int dir[8][2]={{0,1},{1,-1},{-1,-1},{-1,0},{0,-1},{-1,1},{1,0},{1,1}};//用来实现八个方向的遍历
typedef struct Node
{
    int x,y;
}node;
void bfs(int x,int y)
{
    node p,t;
    queue<node> q; //定义队列
    p.x=x;
    p.y=y;
    q.push(p);	//加入初始节点
    while(!q.empty()) //队列为空就搜索结束
    {
        p=q.front(); 
        q.pop();
        for(int i=0;i<8;i++)
        {
            t.x=p.x+dir[i][0];
            t.y=p.y+dir[i][1];
            if(t.x<0||t.x>=n||t.x<0||t.y>=m) //搜索条件不满足
            {
                continue;
            }
            if(!vis[t.x][t.y]&&s[t.x][t.y]=='@')  //满足搜索条件
            {
                vis[t.x][t.y]=1;  //对其进行标记,避免重复搜索
                q.push(t); //添加到队列后面
            }
        }
    }
}
int main()
{
    while(scanf("%d %d",&n,&m)&&(n+m))
    {
        memset(vis,0,sizeof vis);
        cnt=0;
        for(int i=0;i<n;i++)
        {
            scanf("%s",s[i]);
        }
        for(int i=0;i<n;i++)
        {
            for(int j=0;j<m;j++)
            {
                if(!vis[i][j]&&s[i][j]=='@')
                {
                    vis[i][j]=1;
                    cnt++;
                    bfs(i,j);
                }
            }
        }
        printf("%d\n",cnt);
    }
    return 0;
}

可以看出,实际上BFS算法的实现相对DFS还更好理解一点,因为DFS涉及到递归的问题,而BFS算法就是一个很简单的队列的“进入进出”。当然,这只是因为题目相对简单,若是碰到复杂的题目,想要编出正确的BFS算法还是相当有难度的。这边我再给一个例子。

第二个例子

Problem G: 分球

Time Limit: 1 Sec Memory Limit: 64 MB
Submit: 30 Solved: 7

Description

在一个装满财宝的屋子里,有2N个盒子排成一排,除了两个相邻的空盒外,其余的每个盒子里都装有一个金球或银球,总共有N-1个金球和N-1个银球。下面是一个N=5的列子 ,G表示金球,S表示银球。
________________________________________
| G | S | S | G | | | G | S | G | S |
-----------------------------------------
任意2个相邻的非空的盒子里的球可以移动到两个相邻的空盒中,移动不能改变这2个球的顺序。写一个程序,用最小的移动次数把所有的金球都移到所有的银球的左边。

Input

输入包含多组数据。第一行为K,表示数据的总数。 每组数据的第一行是N(3 <= N <=7),第2行是2N个盒子的初始状态。金球用 a表示,银球用b,空盒用空格表示。每2组相邻数据用空行隔开。

Output

对于每一组数据,若无解则输出一行NO SOLUTION,若有解,每行输出移动的步数

Sample Input

3
3
abab

5
abba abab

6
a babbababa

Sample Output

NO SOLUTION
3
4

这道题目是期末考试的压轴题,因为当初没学过搜索算法,所以完全没办法做,最近恰好在研究搜索算法,因此为了弥补一下自己心中的遗憾,就回头做了下这题,确实,这道题目对现在的我而言还是比较难的。。。也是经历了一番波折才将其做出来吧。

这道题目由于是要求最小步,并且设计到搜索,我们可以联想到要使用BFS搜索算法,当然DFS应该也是可以的,不过在这里有可能会超时,最好还是使用BFS算法。

思路大致分成三块:
1.判断当前状态是否符合“左金右银”
2.如何改变小球位置
3.如何对状态进行存储

第一点比较简单,主要通过编写judge函数实现,不过这里有一个细节,就是我一开始把题目意思理解成在空格左边都是金,在空格右边都是银才符合要求,但其实这样是错误的,只要“左金右银就可以了”,中间可以穿插空格,是我想太多了。说到底还是我审题不行。。

第二点的话用string自带的函数就比较容易实现,就是通过substr进行拼接,有了这个函数就相对比较简单了。

第三点是这道题目的关键,我们必须要让步数和当前状态同步存储,这里就需要用到pair模板(当然用结构实现也可以),然后将pair加到队列中就可以了,但是,这个问题还涉及到一个最重要的环节,就是什么时候当前搜索会无法进行下去(注意这里不要和队列为空搞混),这是一个比较难想到的点,其实就是当遇到重复出现的情况的时候,当前搜索就要结束了,开始下一次搜索。而一看到去重,我们就应该可以直接联想到map模板,这个可是去重神器,因此我们将出现过的状态存到map模板里面,就可以很好地实现去重效果。

代码如下

#include<bits/stdc++.h>
using namespace std;
int judge(string x,int len_x)	//判断是否是“左金右银”状态
{
	int i = 0;
	while (x[i]!='b')
	{
		i++;
	}
	for (i; i < 2 * len_x; i++)
	{
		if (x[i] == 'a') return 0;
	}
	return 1;
}
int bfs(string x,int len_x)	
{
	if (judge(x, len_x)) return 0;
	int feet = 0;
	map<string, int> status; //存储搜索过的状态,实现去重效果
	queue<pair<string,int>> q; //定义队列
	pair<string, int> a(x, 0);
	q.push(a); //存入初始状态
	string y;
	int m;
	status[x] = 1;
	while (!q.empty())
	{
		y = (q.front()).first;
		m = (q.front()).second;
		q.pop();
		int blank_index;
		if (judge(y, len_x)) //符合"左金右银",搜索结束,返回搜索次数
		{
			return m;
		}
		for (int i = 0; i < y.length() - 1; i++) //查找空格出现的第一个位置,方便后面移动小球
		{
			if (y[i] == ' ' && y[i + 1] == ' ')  blank_index = i;
		}
		for (int i = 0; i < y.length() - 1; i++) //开始搜索
		{
			if (y[i] != ' ' && y[i + 1] != ' ') //符合搜索条件,交换小球
			{
				string z = ""; //z为小球的交换后的状态,下面通过substr拼接算出
				if (blank_index > i)
				{
					z = y.substr(0, i) + "  " + y.substr(i + 2, blank_index - i - 2) + y.substr(i, 2) + y.substr(blank_index + 2, 100);
				}
				else
				{
					z = y.substr(0, blank_index) + y.substr(i,2)+y.substr(blank_index+2,i-blank_index-2)+"  "+ y.substr(i + 2, 100);
				}
				if (status[z] != 1)  //如果没有存储过这个状态,就将其加到队列后面,并且加入到去重map模板status里面
				{
					status[z] = 1;
					pair<string, int> a(z,m+1);
					q.push(a);
				}
			}
		}
	}
	return -1;
}
int main()
{
	int N;
	while (cin >> N)
	{
		string x;
		int len_x;
		while (N--)
		{
			cin >> len_x;
			getchar();
			getline(cin, x);
			int k = bfs(x, len_x);
			if (k == -1)
			{
				cout << "NO SOLUTION" << endl;
			}
			else
			{
				cout << k<<endl;
			}
		}
	}
}

上述是用BFS实现的,可以看出,如果是一开始接触的话,其实是比较有难度的,在这之后,出于好奇,我又尝试用DFS算法解了下这道题。但很遗憾,我写出来的DFS算法带不动。。。如果有人用DFS做出来了,可以Q我一下,这我确实没有法子。

我的失败代码如下:

#include<bits/stdc++.h>
using namespace std;
int Min;
int len_x;
map<string, int> status;
int judge(string x)
{
	int i = 0;
	while (x[i]!='b')
	{
		i++;
	}
	for (i; i < 2 * len_x; i++)
	{
		if (x[i] == 'a') return 0;
	}
	return 1;
}
void dfs(string x,int n)
{
	if (judge(x))
	{
		if (Min == -1)
		{
			Min = n;
			return;
		}
		else
		{
			if (n < Min) Min = n;
			return;
		}
	}
	int blank_index;
	for (int i = 0; i < x.length() - 1; i++) //查找空格出现的第一个位置,方便后面移动小球
	{
		if (x[i] == ' ' && x[i + 1] == ' ')  blank_index = i;
	}
	for (int i = 0; i < x.length() - 1; i++) //开始搜索
	{
		if (x[i] != ' ' && x[i + 1] != ' ') //符合搜索条件,交换小球
		{
			string z = ""; //z为小球的交换后的状态,下面通过substr拼接算出
			if (blank_index > i)
			{
				z = x.substr(0, i) + "  " + x.substr(i + 2, blank_index - i - 2) + x.substr(i, 2) + x.substr(blank_index + 2, 100);
			}
			else
			{
				z = x.substr(0, blank_index) + x.substr(i, 2) + x.substr(blank_index + 2, i - blank_index - 2) + "  " + x.substr(i + 2, 100);
			}
			if (status[z] != 1)
			{
				status[z] = 1;
				dfs(z, n + 1);
				status[z] = 0;
			}
		}

	}
}
int main()
{
	int N;
	while (cin >> N)
	{
		string x;
		while (N--)
		{
			Min = -1;
			cin >> len_x;
			getchar();
			getline(cin, x);
			if (judge(x))
			{
				cout << 0 << endl;
			}
			else
			{
				status[x] = 1;
				dfs(x, 0);
				if (Min == -1)
				{
					cout << "NO SOLUTION" << endl;
				}
				else
				{
					cout << Min << endl;
				}
			}
		}
	}
}

如果觉得有帮助,可以关注一下我的公众号,我的公众号主要是将这些文章进行美化加工,以更加精美的方式展现出来,同时记录我大学四年的生活,谢谢你们!
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值