八数码问题 BFS+Cantor解决

八数码

题目地址:

Acwing

HDU

在一个 3×3 的网格中,1∼8 这 8 个数字和一个 X 恰好不重不漏地分布在这 3×3 的网格中
在游戏过程中,可以把 X 与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为正确排列

X 与上下左右方向数字交换的行动记录为 udlr
现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列

我花了好几个晚上才完整的写了出来,毕竟还是新手,但独立写出来之后还有一点成就感(当然有参考)。下面就来小小提提我的思路吧。

bfs不仅可以搜索路径,还可以搜索状态。

这是我从黑书上看到的一句话,从后几个晚上便开始了我的不归路。

所以我也用黑书上的思路,__bfs+cantor__解决这道题

这题要寻找最短路径,所以bfs更适合

1. 广度优先搜索 (BFS)

这个思路很好理解

初始状态入队
	while(队列不为空)
		取出队首
		if(找到目标)
			返回答案
		else
			相邻状态入队

伪代码非常清晰,现在我们把__文字展开成代码实现__得到。

首先我们联系一下问题

输入占一行,将 3×3 的初始网格描绘出来。

例如,如果初始网格如下所示:

1 2 3 
x 4 6 
7 5 8 

则输入为:1 2 3 x 4 6 7 5 8

输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。
如果答案不唯一,输出任意一种合法方案即可。
如果不存在解决方案,则输出 unsolvable

我们可以用多种方式存储状态,我这里选择的一维数组。
因为输出行动记录,我的思路就是用一个__结构体__存储__数组__和__上次到这次的行动__,还有__上次状态的地址__。

语言描述有点难懂,现在来看看代码实现。

struct state{
	int num[9];	//数组
	char ch;	//上次到这次的行动
	int re;		//上次状态的地址
};

在这里说到说到上次状态的地址,这是一个__整数__而__不是一个指针__。
这是因为我们使用__数组__模拟bfs中的__队列__,这个__整数__是__数组下标__,
搜索的元素出队后,不会删除,而是数组头指针和尾指针的移动。所以我们可以这样记录地址

更详细的可以学习下队列相关知识。

实现一下,看不懂没关系,稍后会解释(看起来代码很长,其实很多重复的地方,读者可以试着优化)

state bfs()
    //初始状态入队,把初始状态的上状态地址设为-1,以便区别与普通状态
	begin.re = -1;
	q[++tt] = begin;
	while(tt >= hh){
		//是否找到,如果队首的九个数都和目标相同,则为找到。也可用memcmp等函数
		int cnt = 0;
		for(int i = 0;i < 9;++i)
			if(q[hh].num[i] == aim[i])
				cnt++;
		if(cnt == 9) return q[hh];
        //寻找0的位置,我们在输入的时候把x换成0,这样子,方便数组存储
		int z;
		for(z = 0;z < 9;++z)
			if(q[hh].num[z] == 0)
				break;
        //判断相邻状态
		if((z+1)%3){//是否能向左,依题意得2 5 8这三个位置不能向左 
			state tmp;
			tmp.ch = 'r';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z+1]);
			//是否查找过状态,vis定义为  如果找过则返回false,反则true
            //具体代码实现就不给了,因为后面后用一个更nb的函数代替
            if(vis(tmp.num))
                q[++tt] = tmp;
		}
		if(z % 3){//同上
			state tmp;
			tmp.ch = 'l';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z-1]);
			if(vis(tmp.num)) q[++tt] = tmp;;
		}
		if(z > 2){//ts
			state tmp;
			tmp.ch = 'u';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z-3]);
			if(vis(tmp.num)) q[++tt] = tmp;
		}
		if(z < 6){//t
			state tmp;
			tmp.ch = 'd';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z+3]);
			if(vis(tmp.num)) q[++tt] = tmp;
		}
		++hh;//弹出队首,因为我们前面不是取出队首,而是直接用队首,所以在此弹出队首
	}
	return begin;//如果没有找到,返回begin
}

解释: 因为我们用一维数组存储当前格子的状态,也就是(已经把x换成0)

1 2 3
4 5 6
7 8 0

存为 1 2 3 4 5 6 7 8 0 对应的a[0] = 1,a[1] = 2,a[3] = 2 也就是3对应的__位置数组下标__为2

由此可知,当0的__位置数组下标__为2,5,8是,不能往左走
同理 0,1,2时,不能往上 0,3,6不右 6,7,8不下
随后用if条件判断即可。

而行走我们用了swap函数,交换0(即x)的位置数组下标和目标位置的下标,完成一次行走

最后把__上次到这次的行动__和__上次位置地址__存储到tmp上,如果符合条件就入队。

康托展开 (Cantor Expansion)

代码主体部分已经基本完成,但还有很多问题,比如判重函数 vis() 还没有实现,如果没有判重,程序会产生很多无效操作,复杂度大大增加,但如果使用暴力的方法判重,每次把新状态和9! = 362880 个状态对比,可能有9!*9!次检查,必__TLE__

所以我们用到了这种数学方法__“康托展开”__判重

康托展开是一种特殊的哈希函数

实际上,康托展开听起来很高大上,其实就是__把几个数的排列映射到值上__,每个值对应一种排列,如4个数的全排列可以用4! = 24个值表示,见下表

状态Cantor
01230
01321
02132
02313
…………
321023

那如何完成从状态到值的转换呢,当然是有公式滴
X = a n ( n − 1 ) ! + a n − 1 ( n − 2 ) ! + . . . + a 2 × 1 ! + a 1 × 0 ! X = a_n(n-1)! + a_{n-1}(n-2)!+...+a_2\times1! + a_1 \times 0! X=an(n1)!+an1(n2)!+...+a2×1!+a1×0!
其中, a i a_i ai表示原数的第i位在当前为出现的原数中排第几个(从0开始数的)
乱七八糟,对吧

其实这东西真不难,看着唬人而已,我们来展开几个数试试。

0 2 3 1
第4位为0,0排第0个,0个数中没有数出现过, a 4 a_4 a4 = 0 - 0 = 0;
第3位为2,2排第2个,2个数中1个0出现过, a 3 a_3 a3 = 2 - 1 = 2
第2位为3,3排第3个,3个数中2个数(0,2)出现过, a 2 a_2 a2 = 3 - 2 = 1
第1位为1,1排第1个,1个数中1个0出现过, a 1 a_1 a1 = 1 - 1 = 0
所以
X = 0 × 3 ! + 1 × 2 ! + 1 × 1 ! + 0 × 0 ! = 3 X = 0\times3! + 1\times2! + 1\times1! + 0\times0! = 3 X=0×3!+1×2!+1×1!+0×0!=3

1 0 3 2
第4位为1,1排第1个,1个数中没有数出现过, a 4 a_4 a4 = 1 - 0 = 1;
第3位为0,0排第0个,0个数中没有数出现过, a 3 a_3 a3 = 0 - 0 = 0
第2位为3,3排第3个,3个数中2个数(0,1)出现过, a 2 a_2 a2 = 3 - 2 = 1
第3位为2,2排第2个,2个数中2个数(0,1)出现过, a 1 a_1 a1 = 2 - 2 = 0
所以
X = 1 × 3 ! + 0 × 2 ! + 1 × 1 ! + 0 × 0 ! = 7 X = 1\times3! + 0\times2! + 1\times1! + 0\times0! = 7 X=1×3!+0×2!+1×1!+0×0!=7

作了这些练习,有没有一种编程的欲望?下面给出代码实现(我自己写的,当然还有优化空间)

int org[10] = {0,1,2,3,4,5,6,7,8};//原数
int factory[11] = {1,1,2,6,24,120,720,5040,40320,362880};//阶乘打表
//bool basket[10]之前定义在外面,debug找了好久才出错,数组很容易犯这种错

//查看有几个数出现过
inline int g(int x,int * basket){//因为basket为cantor()的成员变量,所以传进来
	int cnt = 0;		
	for(int i = 0;i < x;++i)
		if(basket[i])
			++cnt;
	return cnt;
}
inline int cantor(int * a){
	int x = 0;
    bool basket[10] = {0};//查看有什么数出现过,记得定义为成员变量,并初始化
	for(int i = 0;i < 9;++i){
		x += (a[i] - g(a[i],basket)) * factory[8-i];
		basket[a[i]] = true;//找过的置1
	}
	return x;
}

这就是康托展开,我上面的解释是以编程,即应用的角度出发去理解的。读者也可以思考一下他的数学内涵(可从排列的角度出发)

AC代码

我们的基本思路就是BFS+Cantor解决这玩意,当然还有双向bfs和A*等算法,读者可以尝试,下面给出我的AC代码(优化的可以)

#include <cstdio>
#include <cstring>
#include <utility>
#include <vector>
#include <iostream>
using namespace std;

struct state{
	int num[9];
	char ch;
	int re;
};//定义状态结构体

vector<char> ans;//用vector存答案
state q[666666];//模拟队列
int hh,tt = -1;//头,尾指针
int beg[9];//开始数组
int aim[9] = {1,2,3,4,5,6,7,8,0};//目标数组
int org[9] = {0,1,2,3,4,5,6,7,8};//原数组,Cantor展开用
int factory[11] = {1,1,2,6,24,120,720,5040,40320,362880};//阶乘打表
bool vis[362881];//找过的数放进来

inline int g(int x,int * basket){
	int cnt = 0;
	for(int i = 0;i < x;++i)
		if(basket[i])
			++cnt;
	return cnt;
}
inline bool cantor(int * a){
	int x = 0;
	int basket[9] = {0};
	for(int i = 0;i < 9;++i){
		x += (a[i] - g(a[i],basket)) * factory[8-i];
		basket[a[i]] = true;
	}
	if(vis[x])//判断是否找过
		return false;//找过就false
	vis[x] = true;//没找过就放进来
	return true;//没找过就true
}

state bfs(){
	state begin;
	memcpy(begin.num,beg,sizeof beg);//把begin数组给初始状态
	begin.re = -1;
	q[++tt] = begin;
	while(tt >= hh){
		int cnt = 0;
		for(int i = 0;i < 9;++i)
			if(q[hh].num[i] == aim[i])
				cnt++;
		if(cnt == 9) return q[hh];
		int z;
		for(z = 0;z < 9;++z)
			if(q[hh].num[z] == 0)
				break;
		if((z+1)%3){
			state tmp;
			tmp.ch = 'r';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z+1]);
			if(cantor(tmp.num)) q[++tt] = tmp;
		}
		if(z % 3){
			state tmp;
			tmp.ch = 'l';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z-1]);
			if(cantor(tmp.num)) q[++tt] = tmp;;
		}
		if(z > 2){
			state tmp;
			tmp.ch = 'u';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z-3]);
			if(cantor(tmp.num)) q[++tt] = tmp;
		}
		if(z < 6){
			state tmp;
			tmp.ch = 'd';
			tmp.re = hh;
			memcpy(tmp.num,q[hh].num,sizeof (q[hh].num));
			swap(tmp.num[z],tmp.num[z+3]);
			if(cantor(tmp.num)) q[++tt] = tmp;
		}
		++hh;
	}
	return begin;
}

int main(){
	char chtmp;
	for(int i = 0;i < 9;++i){
		cin >> chtmp;
		if(chtmp == 'x')
			beg[i] = 0;
		else 
			beg[i] = chtmp - '0';
	}//读入begin数组

	cantor(beg);//初始状态放进vis
	state answer = bfs();
	if(answer.re == -1){//如果没找到目标,输出unsolvable
		printf("unsolvable\n");
		return 0;
	}
	while(answer.re != -1){
		ans.push_back(answer.ch);
		answer = q[answer.re];//一层一层向上找,把ch存入答案
	}

	for(auto i = ans.end()-1;i >= ans.begin();--i)
		printf("%c",*i);//逆序输出答案
	puts("");
	return 0;
}

Thats all

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值