BFS:八数码问题求解

八数码原题

剖析一下BFS

        BFS算法是一种图遍历算法,它从起点开始,逐层扩展搜索范围,直到找到目标节点为止。        

        BFS算法一般选择队列作为节点存储的数据结构,我们将搜索目标节点的问题抽象为寻找目标状态,那么队列存储的对象就是每一种状态。

        对于状态的含义与变化过程,BFS算法如下要求(为了讲解得更加透彻,举走迷宫问题为例):

  1. 初始以及其拓展出开的状态都要存储在队列中。在走迷宫问题中,队列q存储已走过的点 ,点代表位置状态,基于一个点可以拓展出它四周的点;
  2. 问题的求解必须有初始状态最终状态。在走迷宫问题中,起点(0,0)便是初始状态,而终点(n,n)是最终状态;
  3. 问题的状态次数是有限的已出现的状态一般需要被记录下来。在走迷宫问题中,dis[i][j]表示从起点到达点(i,j)的距离。当dis的所有元素被初始化为0时,如果dis[i][j]非零时,就说明点(i,j)已被记录过;
  4. 当前状态preState可以拓展出其他状态State,如果State已经出现过或者不符合要求,那么该状态无法加入队列中.

八数码问题的求解 

问题描述

        八数码,在3×3的方格棋盘上,摆放着1到8这八个数码,有1个方格放置字符x,其初始状态如图所示,要求对字符x执行x左移、x右移、x上移和x下移这四个操作使得棋盘从初始状态到目标状态。

        147ac6bb7d3b4fd7ab401c398919f979.png

        基于上述状态能够拓展出如下四种状态:

7af3caa860a84e0dbf6fdf8ee5809ba5.png

         八数码问题要求是,对字符x进行若干次唯一操作,得到目标状态并且求算操作次数: 

9ea05a3f65d745cfa077648e36522153.png

 求解思路

  1. 每一个矩阵的里面的元素能够用一个字符串来存储,例如存储最终状态string s = "12345678x";
  2. 本问题借助state结构体存储每一种状态,state的分量为:s,pos,step.其中,s代表该状态对应的字符串,pos代表x在s字符串中的位置,step代表本状态由初始转台经过了step次操作得来;
  3. 定义一个map<string,bool>st映射,记录该字符串(即新拓展出来的状态)曾经是否出现过,即已出现的状态需要被记录下来;
  4. 当前状态preState包含三个分量{s,pos,step},根据pos与s值,拓展出其他至多四种状态,具体实现将代码的genOtherState函数.

        好啦,大功告成~~👌👌👌其他细节部分见代码后,也许会有更加深刻的体会(代码有详细注释,所谓优秀的代码本身就是学习文档!!!)

代码实现

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<map>
#include<queue>
using namespace std;

//	come from Acwing

//	记录状态
struct state {
	string s;
	int pos;	//	x的位置
	int step;	//	step次交换
};

string str;
map<string, bool>st;	//	记录某个状态是否曾出现
queue<state>q;	//	状态队列

//	产生其他状态
void genOtherState(const state& sta) {
	string s;
	state t;
	//	判断是否能够交换x与它上面字符的位置
	if (sta.pos - 3 >= 0) {
		//	交换两个字符的位置
		s = sta.s;
		swap(s[sta.pos], s[sta.pos - 3]);
		if (!st[s]) {
			q.push(state{ s,sta.pos - 3,sta.step + 1 });
			st[s] = true;
		}
	}

	//	判断是否能够交换x与它下面字符的位置
	if (sta.pos + 3 <= 8) {
		s = sta.s;
		swap(s[sta.pos], s[sta.pos + 3]);
		if (!st[s]) {
			q.push(state{ s,sta.pos + 3,sta.step + 1 });
			st[s] = true;
		}
	}

	// 判断是否能够交换x与它右边字符的位置
	if (sta.pos % 3<=1) {
		s = sta.s;
		swap(s[sta.pos], s[sta.pos + 1]);
		if (!st[s]) {
			q.push(state{ s,sta.pos + 1,sta.step + 1 });
			st[s] = true;
		}
	}

	// 判断是否能够交换x与它左边字符的位置
	if (sta.pos % 3 >= 1) {
		s = sta.s;
		swap(s[sta.pos], s[sta.pos - 1]);
		if (!st[s]) {
			q.push(state{ s,sta.pos - 1,sta.step + 1 });
			st[s] = true;
		}
	}
}

int bfs(int pos) {
	string target = "12345678x";	//	目标状态
	
	// 初始化
	state init = state{ str,pos,0 };
	q.push(init);
	st[str] = true;

	while (q.size()) {
		auto t = q.front();	//	取出队头状态进行拓展
		q.pop();
		if (t.s == target) {
			return t.step;
		}

		//	当前状态拓展出其余状态
		genOtherState(t);
	}

	return -1;
}

int main() {
	int cnt = 9;
	int pos = 0;
	
	//	输入
	for (int i = 0;i <= 8;i++) {
		char a;
		cin >> a;
		if (a == 'x') {
			pos = i;
		}
		str += a;
	}

	int res = bfs(pos);
	cout << res << endl;
	return 0;
}

     😫实在不好意思,上述程序由于状态的存储与状态的拓展过于繁杂,所以导致了Time Limit Exceeded了,如下提供优化状态存储版本的程序如下:

优化版本的八数码算法

#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
#include<string>
#include<queue>
#include <unordered_map>
using namespace std;

/*
	改进的地方:
	1.每一种状态的步数通过unorder_map<string,int>实现映射
	减少了存储状态的内存消耗
	2.采用string的find()函数查找,x的位置;
	3.通过运算直接获取原状态可以拓展的状态
*/

string start;	//	其实状态
string End = "12345678x";	//	最终状态

int bfs() {
	queue<string>q;	//	存储状态
	unordered_map<string, int>d;	//	d.count(t)表示t进入容器的次数

	//	初始化
	q.push(start);
	d[start] = 0;

	while (q.size()) {
		auto t = q.front();
		q.pop();

		int pos = t.find('x');	//	查找x的位置
		int distance = d[t];
		int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };	//	拓展的位移

		if (t == End) {
			return d[t];
		}

		for (int i = 0;i < 4;i++) {
			int a = pos / 3 + dx[i];
			int b = pos % 3 + dy[i];

			if (a >= 0 && a <= 2 && b >= 0 && b <= 2) {
				swap(t[pos], t[3 * a + b]);
				//	如果不是第一次加入队列
				if (!d.count(t)) {
					q.push(t);
					d[t] = distance + 1;
				}
				swap(t[pos], t[3 * a + b]);
			}
		}
	}
	return -1;	//	没有达到最终状态
}

int main() {
	int cnt = 9;

	while (cnt--) {
		char a;
		cin >> a;
		start += a;
	}
	cout << bfs() << endl;
	return 0;
}

总结

        本博客先解释了BFS的存储队列节点的抽象含义,将每一个节点看作一种状态,并从状态存储、状态记录、状态拓展等角度解答了BFS算法如何解决遍历问题。

        紧接着,我们借助BFS算法的状态处理方法给出了八数码问题的求解思路。我们以字符串存储了八数码的每一种状态,每一种状态记录{s,pos,step}三个分量,借助st映射记录新拓展出的状态,并于代码中给出状态拓展的方法。

        实际上,八数码问题只是抽象BFS问题的一种实例化,当我们判断一个问题是否属于BFS问题时,需要判断问题是否存在初始状态与最终状态?每一种状态如何存储,有哪些分量?如何记录每一种状态?每一种状态拓展出其他状态的方式是否是规律的且有限的?掌握了如上的思考方式,相信你能够在下一次遇到或判断一个问题是否属于BFS问题时,你能够更加游刃有余!!!

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值