八数码问题

八数码问题和状态图搜索

八数码问题

BFS我们很熟悉,一般来说在leetcode上多是value类型,其实其处理对象也可以是状态。下面一起来看看八数码问题:
在一个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!=362880个状态,从初始棋盘开始,每次移动到下一个状态,到达目标棋局后终止;
八数码问题是一个经典的BFS问题,从起始位置开始由内向外扩散开来,从而逼近目标,当到达目标时就是步数最短路径。其实BFS也用在权值一样的求最短路径问题中,当然用图也可以搞定,这就是后话了。

回到本题来,在进行BFS遍历时会遍历许多重复的状态,如果不去重那么程序会产生很多无效操作,另外一个巧妙的地方使用快速判重方式。如果在这里采用暴力去重,那么需要将新状态与9!个状态比较,会产生9!X9!次判断,明显不行。

康托去重

这里介绍一个数学方法:康托展开(Cantor Expansion)来去重;
说白了,Cantor去重实际是一个特殊hash函数:

状态 012345678 012345687 012345768 ... 876543210
Cantor 0 1 2 ... 362880-1

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

下面介绍康托的原理:
例子:判断2143是{1, 2, 3, 4}中全排列中第几大数;
计算在2143前面的排列数目,可转换为一下排列和:
1)首位小于2的排列:这里比2小的就只有1,因此后面3位排列为3!=6;可写成1X3!=6
2)首位为2,第二位小于1的数列:没有,亦可写成0X2!=0
3)前两位为21,第三位小于4的所有排列:只有3一个, 1x1!=1
4)前3位位214,第4位小于3的所有排列:无,写为0X0!=0
把一个集合产生的全排列按照字典序排序,第X个排列的计算公式为:
X=a[n]x(n1)!+a[n1]x(n2)!+...+a[i]x(i1)!+...+a[2]x1!+a[1]x0!
其中a[i]表示原数的第i位在当前未出现的元素中排在第几个(从0开始),且有0a[i]<i(1in)
上面去重是Cantor的逆展开:某个集合的全排列,输入数字K,返回第K大的排列;

#include <bits/stdc++.h>
const int LEN = 362880; //总状态数9!=362880;
using namespace std;

struct node
{
    int state[9]; //记录八数码的一个状态;
    int dis;      //记录到起点的距离;
};

int dir[4][2] = {
    {-1, 0},
    {1, 0},
    {0, -1},
    {0, 1},
};

int visited[LEN] = {0};
int start[9];  //起始状态;
int target[9]; //目标状态;
//阶乘计算结果预存;
long int factory[] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320, 362880};

bool Cantor(int str[], int n)
{
    long res = 0;
    for (int i = 0; i < n; i++)
    {
        int counter = 0;
        for (int j = i + 1; j < n; j++)
        {
            if (str[j] < str[i])
                counter++;
        }

        res += counter * factory[n - i - 1];
    }

    if (!visited[res])
    {
        visited[res] = 1;
        return true;
    }
    return false;
}

int bfs()
{
    struct node head;
    memcpy(head.state, start, sizeof(head.state));
    head.dis = 0;
    queue<node> qu;
    Cantor(head.state, 9);
    qu.push(node);

    while (!qu.empty())
    {
        head = qu.front();
        qu.pop();
        int z;
        for (z = 0; z < 9; z++)
        {
            if (head.state[z] == 0)
                break;
        }

        int x = z % 3;
        int y = z / 3;
        for (int i = 0; i < 4; i++)
        {
            int newx = x + dir[i][0];
            int newy = y + dir[i][1];
            int nz = newx + newy * 3;
            if (newx >= 0 && newx < 3 && newy >= 0 && newy < 3)
            {
                struct node newnode;
                memcpy(&newnode, &head, sizeof(struct node));
                swap(newnode.state[z], newnode.state[nz]); //交换0的位置;
                newnode.dis++;
                if (memcmp(newnode.state, target, sizeof(target)) == 0)
                {
                    return newnode.dis;
                }

                if (Cantor(newnode.state, 9))
                    qu.push(newnode);
            }
        }
    }

    return -1;
}

int main()
{
    for (int i = 0; i < 9; i++)
        cin >> start[i];
    for (int i = 0; i < 9; i++)
        cin >> target[i];
    int num = bfs();
    if (num != -1)
        cout << num << endl;
    else
        cout << "Impossible" << endl;

    return 0;
}

下面是OJ的两题,可以切一下
https://blog.csdn.net/aozil_yang/article/details/54696024
https://blog.csdn.net/aozil_yang/article/details/54708637

posted @   zhanghanLeo  阅读(488)  评论(0编辑  收藏  举报
编辑推荐:
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
阅读排行:
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探
· 为什么 退出登录 或 修改密码 无法使 token 失效
点击右上角即可分享
微信分享提示