H - Eight八数码问题(输出操作步骤)

简单搜索&&进阶搜索 - Virtual Judge (vjudge.net)

【题目描述】

在一个3×3的九宫格上,填有1~8八个数字,空余一个位置,例如下图:

123
456
78

在上图中,由于右下角位置是空的,你可以移动数字,比如可以将数字6下移一位:

123123
45645
78786

或者将数字8右移一位:

123123
456456
7878

1~8按顺序排列的情况称为“初始状态”(如最上方图)。“八数码问题”即是求解对于任意的布局,将其移动至“初始状态”的方法。
给定一个现有的九宫格布局,请输出将它移动至初始状态的移动方法的步骤。

【输入】

输入包含多组数据,处理至文件结束。每组数据占一行,包含8个数字和表示空位的‘x’,各项以空格分隔,表示给定的九宫格布局。
例如,对于九宫格

123
46
758

输入应为:1 2 3 x 4 6 7 5 8
注意,输入的数字之间可能有(不止一个?)空格。

【输出】

对于每组输入数据,输出一行,即移动的步骤。向上、下、左、右移动分别用字母u、d、l、r表示;如果给定的布局无法移动至“初始 状态”,请输出unsolvable。
如果有效的移动步骤有多种,输出任意即可。

对于网格形式的图,有以下这些启发函数可以使用:

如果图形中只允许朝上下左右四个方向移动,则可以使用曼哈顿距离(Manhattan distance)。
如果图形中允许朝八个方向移动,则可以使用对角距离。
如果图形中允许朝任何方向移动,则可以使用欧几里得距离(Euclidean distance)。

该题使用曼哈顿距离。

解题思路:

对于这个题目,直接用 bfs 会时间超限,那么可以用 A* 简化它,然后对于判重问题,用 book 数组存储康托展开的值解决,看是否有相同状态。

对于 bfs 的搜索,在数据较为复杂的情况,会有很多无效状态进行广搜,而广搜后面又接着广搜,会降低效率。

A* 的主要作用是:粗略计算出当前的状态到目标状态至少需要走多少步,把无效的点去除。

因为是粗略估计,主要是如果很精确的话,这个时间复杂度也不小,况且还可以直接得到答案,所以 A* 是一个 O(n) 复杂度,不会影响效率。

A* 实现的思路是:f(x)= g(x)+h(x),其中 g(x)是到达该点已经进行的操作数,h(x)是粗略估计到达目标状态的操作数(该数只会比实际操作数大,后面会解释),f(x)是两者之和。计算 h(x)时,是判断它直接到目标的距离(曼哈顿距离),将计算的 f(x)存入open list ,然后从 open list 中选择 f(x) 最小的节点放入closelist中,并将它视作新的父节点,按照以上步骤类推,不断的重复,一直到搜索到终点节点,完成路径搜索。

其中 open list 为待考察节点,close list 为已考察节点,在判断 f(x)的最小值并加入父结点后,其余未用到的结点放入 close list。

康托展开:

首先说说为什么用这个,对应一个迷宫地图来说,判断这个点有没有走过,直接用一个 book 数组记录就好了,但是对于数字地图,这种方法就不太适合。

对于这个题,它存储的是数据,把 3*3 的矩阵转换为一个一维数组(输入的时候实现),然后对于这些排列,就相当于全排列,而康托展开是统计全排列的总数(例如:1 的全排列是 1 种,2 的全排列是 2 种,3 的全排列是 6 种…n 的全排列是 n!种)。每次进行操作后,数组的排列方式发生改变时,它的在排列种对应的康托值也会变化。康托值是其逆序数*对应位置。

对于 1,2,3,4,5 来说,它的康托展开值为 0*4!+0*3!+0*2!+0*1!=0;

对于 4,3,1,5,2 来说,3 * 4!+2 * 3!+0 * 2!+1 * 1!+0 * 0!=85.

首先要明确一点的是,在没有解决方案的时候,也就是逆序对为奇数,此时用 A* 算法的效率和 bfs 的效率一样,所以在判断逆序对后,有解决方案的时候再使用 A* 算法。

步骤:

  1. 输入字符,转换成数组如果是x转换成0
  2. 计算康托值,是要根据数字所在位置的阶乘,所以事先定义一个康托的阶层数组
  3. 然后根据数字的位置统计逆序对
  4. 如果逆序对为奇数,不能到达目标位置
  5. 否则进入A*函数,设置一个由小到大排序的优先队列,重载最小值当f(x)的值相等时,比较h(x)的值
  6. 设置mubiao数组={1,2,3,4,5,6,7,8,0},计算其康托值和
  7. 首先设置结束条件,当前康托值与目标状态的康托值一致时,就说明找到了,输出并结束返回原函数
  8. 将定义的结构体k入队,进入循环用st结构体记录,然后将队头出队
  9. 循环结束的条件是队列为空
  10. 其中g(x)用曼哈顿距离计算
  11. 通过优先队列可以排序,最小值在前
  12. 将最小值作为头结点,接着向下搜索
  13. 题目还需要输出操作步骤,所以用两个数组统计,一个存步骤,一个存下标,输出完当前步骤后,接着用print(f[x])找上一个操作
#include <iostream>
#include <algorithm>
#include <cstring>
#include <queue>
using namespace std;
struct node
{
    int a[3][3];//存当前状态
    int c, x, y;//康托值、行数、列数
    int f, g, h;
    //重载运算符(定义小于号)
    bool operator < (const node& F) const
    {
        //h相等则按g的大小  从小到大排序
        if (h == F.h)
            return g > F.g;
        //否则按h的大小    从小到大排序
        return h > F.h;
    }
};
struct node k;
int fact[] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880 };//康拓进制

int f[362880];//存操作下标
int path[362880];//存操作步骤
//康拓函数
int cantor(int t[][3])
{
    int x[9];
    for (int i = 0; i < 9; i++)
        x[i] = t[i / 3][i % 3];
    int ans = 0;
    for (int i = 0; i <= 8; i++)
    {
        int cnt = 0;
        for (int j = i + 1; j <= 8; j++)
            if (x[i] > x[j])
                cnt++;
        ans += cnt * fact[8 - i];
    }
    return ans;
}
//曼哈顿
int geth(int t[][3])
{
    int ans = 0;
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
        {
            if (t[i][j]==0) 
                continue;
            int x = (t[i][j] - 1) / 3;
            int y = (t[i][j] - 1) % 3;
            ans += abs(x - i) + abs(y - j);
        }
    return ans;
}
//打印答案
void print(int x)
{
    if (k.c != x)
    {
        print(f[x]);
        if (path[x] == 1) cout << "r";
        if (path[x] == 2) cout << "d";
        if (path[x] == 3) cout << "l";
        if (path[x] == 4) cout << "u";
    }
}
int mubiao[][3] = { 1, 2, 3, 4, 5, 6, 7, 8, 0 };
void A()
{
    priority_queue<node> q;
    bool vis[372880] = { 0 };//标记
    int dx[] = { 0, 0, 1, 0, -1 };
    int dy[] = { 0, 1, 0, -1, 0 };
    //每个数的康拓值
    int target = cantor(mubiao);
    k.c = cantor(k.a);
    vis[k.c] = true;
    q.push(k);
    while (!q.empty())
    {
        struct node st = q.top();
        q.pop();
        if (target == st.c)//到达目标状态
        {
            print(st.c);
            printf("\n");
            return;
        }
        for (int i = 1; i <= 4; i++)
        {
            struct node ed = st;
            ed.x = ed.x + dx[i];
            ed.y = ed.y+dy[i];
            if (0 <= ed.x && ed.x < 3 && 0 <= ed.y && ed.y < 3)
            {
                swap(ed.a[ed.x][ed.y], ed.a[st.x][st.y]);
                ed.c = cantor(ed.a);
                if (!vis[ed.c])
                {
                    vis[ed.c] = true;
                    ed.g++; ed.h = geth(ed.a); ed.f = ed.g + ed.h;
                    f[ed.c] = st.c;
                    path[ed.c] = i;
                    q.push(ed);
                }
            }
        }
    }
}
int main()
{
    char ch;
    while (cin<<ch)
    {
        if (ch == 'x')
            k.x = 0, k.y = 0, k.a[0][0] = 0;
        else
            k.a[0][0] = ch - '0';
        //i/3和i%3分别代表行和列
        for (int i = 1; i < 9; i++)
        {
            cin >> ch;
            if (ch == 'x')
                k.x = i / 3, k.y = i % 3, k.a[i / 3][i % 3] = 0;
            else
                k.a[i / 3][i % 3] = ch - '0';
        }

        //逆序对
        int seq[9];
        for (int i = 0; i < 9; i++) 
        {
            seq[i] = k.a[i / 3][i % 3];
        }
        int ans = 0;
        for (int i = 0; i < 9; i++) 
        {
            for (int j = i + 1; j < 9; j++) 
            {
                if (seq[i] == 0 || seq[j] == 0)
                    continue;
                if (seq[i] > seq[j])
                    ans++;
            }
        }
        if (ans%2==1)
            printf("unsolvable\n");
        else
        { 
            //预估价值
            k.g = 0;
            k.h = geth(k.a);
            k.f = k.g + k.h;
            A();
        }
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

明里灰

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

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

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

打赏作者

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

抵扣说明:

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

余额充值