算法入门到进阶(三)——搜索技术(八数码问题和状态图搜索)


基本概念

BFS搜索处理的对象不仅可以是一个数,还可以是一种“状态”。八数码问题是典型的状态图搜索问题

八数码问题

在一个3x3的棋盘上放置编号为1~8的8个方块,每个方块占一格,另外还有一个方块有一个空格。与空格相邻的数字方块可以移动到空格里。任务1:指定初始棋局和目标棋局(如下图)计算出最少移动步数;任务2:输出数码的移动序列
在这里插入图片描述把空格看成0,一共有9个数字。
输入样例:
1 2 3 0 8 4 7 6 5
1 0 3 8 2 4 7 6 5
输出样例:
2

八数码问题思路

针对上面这个问题,把一个棋盘看作一个状态图,总共有9!=362 880个状态。从初始棋局开始,每次移动转到下一个状态,到达目标棋局后停止。

八数码问题是一个经典的BFS问题。前面的博客中提到BFS是从近到远的扩散过程,适合解决最短距离问题。八数码从初始状态出发,每次转移都是逐步逼近目标状态。每转移一次,步数加一,当到达目标时,经过的步数就是最短路径。

如下图。是样例的转换过程。该图中起点为(A,0),A表示状态,即{1 2 3 0 8 4 7 6 5}这个棋局;0是距离起点的步数。从初始状态A出发,移动数字0到邻居位置,按左,上,右,下的顺时针顺序,有3个转移状态B,C,D;目标状态F,停止。

在这里插入图片描述
用队列描述这个BFS过程:

  1. A进队,当前队列是{A};
  2. A出队,A的邻居B,C,D进队,当前队列是{B,C,D}。步数为1;
  3. B出队,E进队,当前队列是{C,D,E},E的步数为2;
  4. C出队,转移到F,检验F是目标状态,停止,输出F的步数2。
    仔细分析上面的过程,发现从B状态出发实际上有E,X两个转移方向,而X正好是初始状态A,重复了。同理Y状态也是重复的。如果不去掉这些重复状态,程序会产生很多无效操作,复杂度大大增加。因此,八数码的重要问题其实是判重。

如果用暴力法判重,每次把新状态与9!=362880个状态进行对比,可能有9!x9!次检查,不可行。因此需要一个快速的判重方法

下面针对八数码中的判重问题,使用一个数学方法来解决问题“康托拓展(Cantor Expansion)”来判重

康托展开

康托展开是一种特殊的哈希函数。在本题中康托展开完成了如下表的工作。
在这里插入图片描述第一行是0~8这9个数字的全排列,共9!=362880个,按从小到大排序。第2行是每个排列对应的位置,例如最小的{012345678}在第0个位置,最大的{876543210}在最后的362800-1这个位置。

函数Cantor()实现的功能是:输入一个排列,即第一行的某个排列,计算出与它的Cantor值,即第二行对应的数。

Cantor()复杂度为O(n^2),n是集合中元素的个数。在本题中,完成搜索和判重的总复杂度是O(n ! n^2),远比暴力判重的总复杂度O(n! n!)小。

有了这个函数,八数码的程序能很快判重:每转移到一个新状态,就用Cantor()判断这个状态是否处理过,如果处理过,则不转移。

下面举列讲解康托展开的原理。

康托展开的原理

案例:判断2143是{1,2,3,4}的全排类中第几大的数

题目

判断2143是{1,2,3,4}的全排类中第几大的数

思路

计算排在2143前面的排列数目,可以将问题转化为以下排列的和:

  1. 首位小于2的所有排列。比2小的只有1一个数,后面3个数的排列有3x2x1=3!个(即1234,1243,1324,1342,1423,1432),写成1x3!=6。
  2. 首位为2、第2位小于1的所有全排列。无,写成0x2!=0。
  3. 前两位为21,第三位小于4的所有全排列。只有3一个数(即2134),写成1x1!=1.
  4. 前3位为214,第4位小于3的所有全排列。无,写成0x0!=0。

求和的7,所有2143是第8大的数。如果用int visited[24]数组记录各排列的位置,{2143}就是visited[7];第一次访问这个排列时,置visited[7]=1;当再次访问这个排列的时候发现visited[7]=1,说明已经访问过了,判重。
根据上面的推导式可以得到康托展开式。
把一个集合产生的全排列按字典排序,第X个排列的计算公式如下:
X=a[n]x(n-1)! + a[n-1]x(n-2)!+…+a[i]x(i-1!)+…a[2]x1!+a[1]x0![1]。
其中,a[i]为当前未出现的元素排在第几个(从0开始),并且有0<=a[i]<i(1<=i<=n)。
上述过程的反过程是康托逆展开:某个集合的全排列,输入一个数字k,返回第k大的排列。
下面的源代码程序用“BFS+Cantor”解决了八数码问题,其中BFS用STL的queue实现

源码(8数码问题)

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

const int LEN = 362880; //共9!种状态
struct node {
	int state[9];  //记录一个八数码的排列,即一个状态
	int dis;	//记录到起点的距离
};

int dir[4][2] = {
	{-1,0},//向左
	{0,-1},//向上
	{1,0},//向右
	{0,1}//向下
};

int visited[LEN] = { 0 };  //与每个状态对应的记录,Cantor()函数对它的置数,并重判
int start[9]; //开始状态
int goal[9];//目标状态

long int factory[] = { 1,1,2,6,24,120,720,5040,40320,362880 };//Cantor()用到的常数

//使用康托展开判重
bool Cantor(int str[], int n) {
	long result = 0;
	for (int i = 0; i < n; i++) {
		int counted = 0;
		for (int j = i + 1; j < n; j++) {
			if (str[i] > str[j]) {//当前未出现的元素排在第几个
				counted++;
			}
		}
		result += counted * factory[n - i - 1];//康托拓展公式
	}
	if (!visited[result]) {
		visited[result] = 1;//没有被访问过
		return 1;
	}
	else {
		return 0;
	}
}

int BFS() {
	node head;
	memcpy(head.state, start, sizeof(head.state));//复制起点状态
	head.dis = 0;
	queue<node>q;//队列的内容是记录状态
	Cantor(head.state, 9);//用康托展开判重,目的是对起点的visited[]赋初值
	q.push(head);//第一个进队列的是起点状态
	while (!q.empty()) {
		head = q.front();
		q.pop();//出队列
		cout << "出队:";//打印输出结果
		for (int i = 0; i < 9; i++) {
			cout << head.state[i] << " ";
		}
		cout << endl;
		int z;
		for ( z = 0; z < 9; z++) {//找到这个状态元素0的位置
			if (head.state[z] == 0) {
				break;
			}
		}
		//将一维转化为二维,方便表达左上右下四个方向的移动表示,左上角是(0,0)
		int x = z % 3;  //0的横坐标
		int y = z / 3;//0的纵坐标
		for (int i = 0; i < 4; i++) {//操作0进行上下左右,4个方向的0移动
			int newx = x + dir[i][0];//左右移
			int newy = y + dir[i][1];//上下移
			int nz = newx + 3 * newy;//再转换为一维方便进行条件判断
			if (newx >= 0 && newx < 3 && newy >= 0 && newy < 3) {//未出界
				node newnode;//创建一个新节点
				memcpy(&newnode, &head, sizeof(struct node));//复制新状态
				swap(newnode.state[z], newnode.state[nz]);//把0的位置移动到新位置
				newnode.dis++;
				if (memcmp(newnode.state, goal, sizeof(goal)) == 0) {//与目标状态一样
					return newnode.dis;//输出结果
				}
				if (Cantor(newnode.state, 9)) {//判断是否重复,如果没放入队列
					q.push(newnode);
				}
			}
		}
	}
	return -1;
	
}

int main() {
	cout << "请输入初始状态:";
	for (int i = 0; i < 9; i++) {
		cin >> start[i];
	}
	cout << "请输入目标状态:";
	for (int i = 0; i < 9; i++) {
		cin >> goal[i];
	}
	int result_num = BFS();
	if (result_num != -1) {
		cout << "result:" << result_num << endl;
	}
	else {
		cout << "Impossible" << endl;
	}
	system("pause");
	return 0;
}

运行结果

在这里插入图片描述

分析

下面对每个代码进行分析,细节很多,所以希望大家自己也能敲以下。加深记忆和理解
在这里插入图片描述在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

拓展提示:15数码的问题

八数码问题只有9!中状态,对于更大的需求,例如4x4棋盘的15数码问题,有16!大约2x10^13中状态,如果还用数组就肯定不够了,就需要更好的算法,这里给大家推荐一个连接,大家可以了解一下
八数码的多种解法

总结

下一篇博客是BFS与A*算法。
其实到现在相信大家也已经明白了BFS算法,从老鼠走迷宫,黑红瓷砖,八数码。对于BFS其实就可以理解为使用的队列来抽象扩散的过程,这个扩散过程也有树的感觉,就是说由一点散发。所以我想称为广度优先算法,正如其意,通过外围的大量的数据进行测试,往目标不断地缩小范围,以找到我们最终地答案。在找到这个答案的时候,通过我们从外围的数据测试做下的标记,就找到了它的最短路径。

  • 5
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Jacky~~

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

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

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

打赏作者

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

抵扣说明:

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

余额充值