acwing—845八数码的详细讲解【化简成迷宫来理解】

其实走迷宫和八数码是本质相同的题目,但之所以get不到是因为前者很容易想象出来,但后者的迷宫形式很抽象,那么我今天就将这两道题的本质挖出来,对比一下,想象部分人会豁然贯通的;

先放一下题目,这里为了减少文章的长度就直接放题目链接了;

AcWing 845. 八数码 - AcWing

844. 走迷宫 - AcWing题库

先讲简单的走迷宫,我们先来确认一下对象有哪些,对象又有哪些属性;

对象——方形位置。

属性一:相对位置

因为每个方形都对其他方形有唯一的相对位置,虽然这个属性看起来很抽象,但是我们可以用二维数组就能轻易解决这个问题,那么好,我们找到一个容器存储地图了。【讲得复杂一点是为了迎合后面八数码做铺垫】

属性二:有没有空地

我们可以将地图拆分为多个方形位置,其中有的位置是墙,有的位置是空地,那么有没有墙就是方形位置的一个属性,那么我们给空格和墙于不同的数字来区分二者。

属性三:是否走过

这个我们可以用另外一个二维数组来存储每个方形位置是否走过;

属性四:距离起点的距离

这个仍然要一个二维数组来存储;

那每个方形位置就有四个属性了,有了这四个属性就可以完成这个走迷宫的题目了;

我先方一下具体的步骤:

#include<iostream>
#include<queue>
#include<vector>
using namespace std;

int ....//定义每个方位置的属性

queue<...>  //定义队列

void bfs(....) {


	int dx,dy;//定义偏移量

	while ()//当队列为空时退出
	{

		for (...)//枚举4个方向
		{
			if ()//筛选满足条件方形位置
			{

				....//其他操作

				queue.push()//插入新元素

			}
		}
		
		queue.pop()//弹出对头

	}

	return//返回结果

}



int main() {

	for (.....)
		cin >> ....//接收数据

	


	cout<< bfs(....);//输出结果;


}

然后是具体的代码实现,走迷宫的思路大家应该是都明白了【毕竟都开始写八数码了】,所以直接给代码就行了

#include<iostream>
#include<queue>
using namespace std;

const int N = 110;

typedef pair<int, int> two;

int map[N][N], tra[N][N], d[N][N];//map对应的是相对位置和有没有空地属性,而tra和d分别对应是否走过和相对起点的距离属性
 
two path[N][N];//里面存在对应点的上一个点的坐标,比如path[2][6]里面有{1,6},说明{2,6}点的上一个点是{1,6}

queue<two> que;//队列里存的是没个点的坐标

int n, m;//地图长宽

int bfs() {

	int x = 0, y = 0;
	que.push({ x,y });//在队列中插入起点

	int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };//偏移量

	while (que.size()) {//当队列为空时就可以退出了

		tra[que.front().first][que.front().second] = 1;//标记头元素已经走过了

		for (int i = 0; i < 4; i++) {

			x = que.front().first + dx[i], y = que.front().second + dy[i];//{x,y}对应的是头元素上下左右四个方向的坐标

			if (x >= 0 && x < n&&y >= 0 && y < m&&map[x][y] != 1 && tra[x][y] != 1) {//判断点是否满足条件

				path[x][y].first = que.front().first; path[x][y].second = que.front().second;//标记上一个点的坐标

				tra[x][y] = 1;//标记满足条件的点已经选了

				que.push({ x,y });//将该点插入队列

				d[x][y] = d[que.front().first][que.front().second] + 1;//更新离起点的距离
			}
		}
		que.pop();
	}
	x = n - 1, y = m - 1;

	while (x || y) {//输出最短路径
		cout << path[x][y].first << ' ' << path[x][y].second << endl;
		x = path[x][y].first, y = path[x][y].second;
	}

	return d[n - 1][m - 1];//返回结果
}



int main() {
	cin >> n >> m;

	for (int i = 0; i < n; i++) {//接收数据

		for (int j = 0; j < m; j++)
			scanf("%d", &map[i][j]);

	}

	cout << bfs();

	return 0;
}

那么现在来讲一下八数码吧;

八数码的本质和迷宫是一样的,本菜鸟问你,迷宫插入队列里的元素是不是某一个点的上下左右四个方向的坐标,其本质是不是一个状态转化为另一个状态?八数码没移动一下是不是由一个状态转变为另一个状态?很明显答案是肯定的,都是由一个状态转变为另一个状态,相对地说,一个状态又可以由不同的状态转变过来【这对应了在迷宫中一个点走向多个其他点,一个点可以通过多个其他点到达——哇,本菜鸟的这句话都让本菜鸟豁然贯通了:)】。

可以想象一下,将八数码由一个状态通过移动一次而转变为另一个状态想象为在迷宫从一个点走到另一个点的过程,是不是就理解了?说实话还是比较难理解,为什么呢?因为我们擅长详细想象直观的东西,而且八数码迷宫的终点又是不确定的,难以想象的,相信大家能通过不断的努力来想明白的:);

那么本菜鸟来描述一下没个状态的属性;

属性一:方阵

这个很好理解,就是每个状态里的每个数的相对位置,可以用一个二维数组存储

属性二:身份

首先为什么我们要有身份这个属性?因为有了身份,我们才能知道哪些状态是走过的,哪些又没有,这个对应的是走迷宫里的标记那一步;

在走迷宫中对应相识的属性是相对位置,因为一个点相对其他位置都有一个独一无二的相对位,所以每个方形位置都有一个独一无二的身份【独一无二的坐标】,而八数码就没有【因为八数码没有坐标的概念,相信这也是八数码难理解的一个原因】,那么我们该如何描述一个状态的身份呢?很简单,我们只要把每个状态的方阵属性转化为一个10进制数就行了,为什么转化为一个十进制数就是唯一的身份了呢?请参考一个二进制数对应唯一一个10进制数;

属性三:从起点状态到当前状态用了多少步

这个很好理解,对应走迷宫里的离起点的属性;

属性四:x的坐标;

要记录当前状态里的‘x'的坐标,要不然不知道可移动的上下左右的位置在哪。

总结属性的最好办法就是结构体

typedef struct {
	char a[3][3];//存储每个字符的相对位置
	int p[3];//存储‘x’字符的坐标
	int num = 0;//通过多少步才转化到当前状态
	int jud;//存储每个状态的身份
}ib;//状态结构体

那么剩下的就算裸的走迷宫了,步骤和走迷宫一模一样,另外一个部分还有怎么鉴别身份了,这里本菜鸟用的是hash表,用的是拉链法,直接用vector<vetcor<int>> 来存hash表,不知道hash表的同学可以在我的另外一个博客看一下,里面有关于拉链hash表的解释;

(39条消息) 关于kmp算法的next数组化简的讲解,以及kmp的替代品“哈希表”_刘欢明的博客-CSDN博客

那么接下来就直接放代码+注释了

#include<iostream>
#include<queue>
#include<vector>
using namespace std;

const int N = 1000003;

typedef struct {
	char a[3][3];
	int p[3];
	int num = 0;
	int jud;
}ib;

queue<ib> ad;//创建队列

ib st;//初始状态

vector<vector<int>> pre(N);//hash表

int ans;

bool isorno(int jud) {//鉴别身份函数

	int k = jud % N;//找到身份码的位置

	for (int i = 0; i < pre[k].size(); i++) {//搜索该位置是否存在形参身份码
		if (pre[k][i] == jud)
			return false;
	}

	pre[k].push_back(jud);
	return true;
}


void bfs(ib one) {

	ad.push({ one });//输入初始状态

	int dx[4] = { -1,0,1,0 }, dy[4] = { 0,1,0,-1 };//定义偏移量

	while (ad.size()) {//当队列为空时说明到达不了终点状态,因为到达不了起点,根据抽屉原理那么就会陷入某一个变换周期中,身份码会无限重复

		ib mid;//定义由某一个状态通过移动转化的状态,因为这里的状态不比迷宫里,迷宫的状态简单,但是这里复杂,所以要特别定义一个状态来存储

		if (ad.front().jud == 123456789) {//123456789是目标状态是身份,如果找到这个身份,那么就返回答案就行了
			ans = ad.front().num; return;
		}

		for (int i = 0; i < 4; i++) {//搜索上下左右移动得到的状态

			int x = ad.front().p[1] + dx[i], y = ad.front().p[2] + dy[i];//得到的状态里‘x'的坐标

			if (x >= 0 && x < 3 && y >= 0 && y < 3) {

				swap(ad.front().a[x][y], ad.front().a[ad.front().p[1]][ad.front().p[2]]);//交换‘x’与其他字符

				int jud = 0;//jud存储的是交换后的身份码

				for (int i = 0; i < 3; i++) {
					for (int j = 0; j < 3; j++) {
						if (ad.front().a[i][j] != 'x')
							jud = jud * 10 + ad.front().a[i][j] - '0';
						else
							jud = jud * 10 + 9;//注意这里‘x’对应的是9,其他对应的是1~8
					}
				}

				if (isorno(jud) == 0) {//如果该身份已经存在,就不用重复存储该身份了,对应走迷宫里不要走已经走过的格子

					swap(ad.front().a[x][y], ad.front().a[ad.front().p[1]][ad.front().p[2]]);//还原
					continue;

				}

				//接下来的4步是本菜鸟称为制作状态

				mid.jud = jud;//存储身份码

				mid.p[1] = x, mid.p[2] = y;//存储交换后‘x’的新坐标

				for (int i = 0; i < 3; i++)
					for (int j = 0; j < 3; j++) {
						mid.a[i][j] = ad.front().a[i][j];//存储交换后每个字符的新位置
					}

				mid.num = ad.front().num + 1;//更新距离起点状态的位置

				
				swap(ad.front().a[x][y], ad.front().a[ad.front().p[1]][ad.front().p[2]]);//还原

				ad.push({ mid });//将新状态插入队列

			}
		}

		ad.pop();//删除队列头元素
	}


}

int main() {

	for (int i = 0; i < 3; i++)//接收头状态
		for (int j = 0; j < 3; j++) {
			cin >> st.a[i][j];
			if (st.a[i][j] == 'x')
				st.p[1] = i, st.p[2] = j;
		}

	bfs(st);

	if (ad.size())//输出答案
		cout << ans;
	else
		cout << -1;
}

有所收获的话可以给个大拇指哦:)

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值