题解 P1379 【八数码难题】

前言:对于八数码难题这道经典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;
}

这就是本蒟蒻的见解,希望能对大家有些帮助

如果文章有问题,或者那些地方不懂,可以私信发给我

转载于:https://www.cnblogs.com/Call-me-zhz/p/11287360.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值