前言:对于八数码难题这道经典bfs的题目,(这是我听学长讲的),我花了不止多久的时间才过了它。
思路:首先,我们可以用bfs,在队列中存储每一步的状态,并将这一个状态取hash值,也就是众位大佬讲的康托展开,如果当某一个状态的hash值已经等于了目标状态的hash值,那么直接输出它的步数即可。(因为广搜有一个第一个搜到的目标状态必定是最优的特性)
补充——康托展开(以下内容选自度娘):
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。
康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
康托展开运算:
\(X=a_n(n-1)!+a_{n-1}(n-2)!+...+a_1\cdot0!\)
其中\(a_i\)为整数,并且\(0\leq a_i<i,1\leq i \leq n\)。
\(a_i\)表示原数的第\(i\)位在当前未出现的元素中是排在第几个
举个例子说明:
在\((1,2,3,4,5)\) \(5\)个数的排列组合中,计算 \(34152\) 的康托展开值。
首位是 \(3\) ,则小于 \(3\) 的数有两个,为 \(1\) 和 \(2\) ,则首位小于 \(3\) 的所有排列组合为 \(a[5]\times(5-1)!\)
第二位是 \(4\),则小于 \(4\) 的数有两个,为 \(1\) 和 \(2\),\(a[5]=2\) ,注意这里 \(3\) 并不能算,因为 \(3\) 已经在第一位,所以其实计算的是在第二位之后小于4的个数。因此\(a[4]=2\)。
第三位是 \(1\),则在其之后小于 \(1\) 的数有 \(0\) 个,所以 $a[3]=0 $。
第四位是 \(5\),则在其之后小于 \(5\) 的数有 \(1\) 个,为 \(2\),所以 \(a[2]=1\)。
最后一位就不用计算啦,因为在它之后已经没有数了,所以 \(a[1]\) 固定为 \(0\)
根据公式:
\(X=2\times 4!+2\times3!+0\times2!+1\times1!+0\times0!=61\)
所以 \(34152\)的康托展开值是 \(61\) (代码实现就在本题中有)
回归正题
根据上面我们的思路,我们把代码实现出来就是这样:
#include <iostream>
#include <cstdio>
#include <cstring>
using namespace std;
typedef unsigned long long ull; //纯属闲得蛋痛
ull end;
int book[3000000]; //判断每一个状态是否出现过,也就是排重(值可以不开这么大)
int v1[4]={0,1,0,-1}; //方向数组,枚举0可以走的四个方向
int v2[4]={1,0,-1,0};
struct node
{
int x,y,kkk; //x,y表示0在a数组中的下标,kkk表示当前的步数
ull hash1; //康托展开的哈希值
int a[4][4]; //状态数组
};
node que[2000001]; //结构体
ull hash1(char s[])//康托展开的函数
{
int f[9]={0,1,2,6,24,120,720,5040,40320}; //先将每一个阶乘的值存下来,方便直接运算
int book[9]={0}; //判断每一个数是否已经出现
ull ans=0,x=8;
for(int i=0;i<strlen(s);i++) //将传来的字符数组遍历一遍
{
int num=0; //num存储在第i个数前的数的数目
book[s[i]-'0']=1; //先标记第s[i]个数已经出现
for(int j=0;j<s[i]-'0';j++) //遍历s[i]之前的数
if(!book[j]) //如果这个数没出现过,那么num++
num++;
ans+=num*f[x--]; //康托展开的公式,每一项等于这一项数前未出现过的数乘以这个数位数的阶乘
}
return ans; //返回康托展开的值
}
int main()
{
int head=1,tail=2;
int i,j,k,n;
char s[9],ss[9];
scanf("%s",s);
end=hash1("123804765"); //首先将目标状态的值存下来
que[head].kkk=0; //步数初始化为0
que[head].hash1=hash1(s); //将初始状态的哈希值放入hash1
book[que[head].hash1]=1; //标记目标状态已经出现
for(i=1;i<=3;i++)
for(j=1;j<=3;j++)
{
que[head].a[i][j]=s[(i-1)*3+j-1]-'0';
if(que[head].a[i][j]==0)
{
que[head].x=i;
que[head].y=j;
}
}//将初始状态存进二维数组,并记录下0的位置
while(head<=tail) //bfs
{
if(que[head].hash1==end) //如果当前状态的值等于目标状态的值,那么就输出步数
{
printf("%d",que[head].kkk);
return 0;
}
for(k=0;k<4;k++) //否则,枚举四个方向
{
char ch[9]={0}; //用来转换的字符串
int tx=que[head].x+v1[k],ty=que[head].y+v2[k];//0移动后的位置
if(tx<1 || tx>3 || ty<1 || ty>3) continue; //如果移动出了边界,那么这个状态肯定不合法
for(i=1;i<=3;i++)
for(j=1;j<=3;j++)
que[tail].a[i][j]=que[head].a[i][j];
swap(que[tail].a[tx][ty],que[tail].a[que[head].x][que[head].y]); //将上一个状态转移过来,再转换位置
for(i=1;i<=3;i++)
for(j=1;j<=3;j++)
ch[(i-1)*3+j-1]=que[tail].a[i][j]+'0'; //将二维数组存进字符数组
ull ans=hash1(ch); //获取这个二位数组的哈希值(康托展开值)
if(!book[ans]) //如果这个状态没有出现过,那么将他加入队列
{
book[ans]=1; //标记这个状态出现过
que[tail].hash1=ans; //记录下它的哈希值
que[tail].x=tx; que[tail].y=ty; //存下目前状态0的位置
que[tail].kkk=que[head].kkk+1; //将步数+1
tail++;
}
}
head++; //千万要加上这句话,不然就会死循环
}
return 0;
}