Files
python/TangDou/AcWing/BFS/AStar/179.md
HuangHai 1f397eca87 'commit'
2025-08-30 18:35:01 +08:00

18 KiB
Raw Blame History

AcWing 179 八数码

题目传送门

一、题目描述

在一个 3×3 的网格中,$1$∼8 这 8 个数字和一个 X 恰好不重不漏地分布在这 3×3 的网格中。

例如:

1 2 3
X 4 6
7 5 8

在游戏过程中,可以把 X 与其上、下、左、右四个方向之一的数字交换(如果存在)。

我们的目的是通过交换,使得网格变为如下排列(称为正确排列):

1 2 3
4 5 6
7 8 X

例如,示例中图形就可以通过让 X 先后与右、下、右三个方向的数字交换成功得到正确排列。

交换过程如下:

1 2 3   1 2 3   1 2 3   1 2 3
X 4 6   4 X 6   4 5 6   4 5 6
7 5 8   7 5 8   7 X 8   7 8 X

把 X 与上下左右方向数字交换的行动记录为 u、d、l、r。

现在,给你一个初始网格,请你通过最少的移动次数,得到正确排列。

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

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

1 2 3 
x 4 6 
7 5 8 

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

输出格式

输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。

如果答案不唯一,输出任意一种合法方案即可。

如果不存在解决方案,则输出 unsolvable。

二、八数码定理

  • 当初始状态棋局的棋子数列的 逆序数 是 奇数 时,八数码问题 无解
  • 当初始状态棋局的棋子数列的 逆序数 是 偶数 时,八数码问题 有解

### 三、朴素版本$bfs$+无路径

#include <bits/stdc++.h>
using namespace std;
typedef unordered_map<string, int> MSI;
//无脑BFS解法
string st, ed = "12345678x";

//上右下左 四个方向
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};

int bfs() {
    queue<string> q;
    MSI dist; //标识某个状态是否用过,如果用过,那么记录的最短距离是多少

    //入队列
    q.push(st);
    dist[st] = 0; //记录出发点最短距离

    //开始bfs
    while (q.size()) {
        //取出队头字符串
        string t = q.front();
        q.pop();

        //如果到达最终状态,结束
        if (t == ed) break;
        int d = dist[t];                              //现在记录的最短路径长度

        int k = t.find('x');                          //在字符串t中查找x的索引位置
        int tx = k / 3, ty = k % 3;                   //映射的二维数组中的行与列
        for (int i = 0; i < 4; i++) {                 //枚举四个方向
            int x = tx + dx[i], y = ty + dy[i];       //目标位置
            if (x < 0 || x >= 3 || y < 0 || y >= 3) continue; //出界
            string v = t;
            swap(v[x * 3 + y], v[k]); // x与目标位置交换一下
            if (!dist.count(v)) {     //如果t字符串出现过,注意C++取unorderd_map是否存在元素时,需要用count
                dist[v] = d + 1;      //记录是上一个距离+1得到的
                q.push(v);            //新位置进入队列
            }
        }
    }
    //因为通过八数码定理已经排除掉了无解的情况,到这里肯定有解,输出最终的状态最短距离即可
    return dist[ed];
}

int main() {
    //加快读入
    cin.tie(0), ios::sync_with_stdio(false);

    //原始输入是用的一个字符一个空格的形式,无法使用string进行读入
    char c;
    for (int i = 0; i < 9; i++) cin >> c, st += c;

    //八数码定理:检查逆序对数量
    int nums = 0;
    for (int i = 0; i < 9; i++) {
        if (st[i] == 'x') continue; //保证不是x
        for (int j = i + 1; j < 9; j++) {
            if (st[j] == 'x') continue; //保证不是x
            if (st[j] < st[i]) nums++;  //逆序数
        }
    }
    //如果逆序对数量是奇数个,则输出-1
    if (nums & 1)
        puts("-1");
    else //否则通过bfs输出最短距离
        printf("%d\n", bfs());

    return 0;
}

### 四、朴素版本$bfs$+有路径

#include <bits/stdc++.h>
using namespace std;
typedef unordered_map<string, int> MSI;
typedef unordered_map<string, pair<char, string>> MSP;
//无脑BFS解法
// 	2997 ms 可以AC掉本题
string st, ed = "12345678x";
//上右下左 四个方向
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};
char op[] = {'u', 'r', 'd', 'l'};
MSP pre; //字符串,前驱操作符,前驱字符串

void bfs() {
    queue<string> q;
    MSI dist; //标识某个状态是否用过,如果用过,那么记录的最短距离是多少

    //入队列
    q.push(st);
    dist[st] = 0; //记录出发点最短距离

    //开始bfs
    while (q.size()) {
        //取出队头字符串
        string t = q.front();
        q.pop();

        //如果到达最终状态,结束
        if (t == ed) {
            string res;
            while (ed != st) {
                res += pre[ed].first;
                ed = pre[ed].second;
            }
            reverse(res.begin(), res.end());
            cout << res << endl;
            break;
        }
        int d = dist[t]; //现在记录的最短路径长度

        int k = t.find('x');                                  //在字符串t中查找x的索引位置
        int tx = k / 3, ty = k % 3;                           //映射的二维数组中的行与列
        for (int i = 0; i < 4; i++) {                         //枚举四个方向
            int x = tx + dx[i], y = ty + dy[i];               //目标位置
            if (x < 0 || x >= 3 || y < 0 || y >= 3) continue; //出界

            string v = t;
            swap(v[x * 3 + y], v[k]); // x与目标位置交换一下
            if (!dist.count(v)) {     //如果t字符串出现过,注意C++取unorderd_map是否存在元素时,需要用count
                dist[v] = d + 1;      //记录是上一个距离+1得到的
                q.push(v);            //新位置进入队列
                pre[v] = {op[i], t};  //记录v这个字符串的前驱字符串是t,并且,是通过操作符op[i]转化而来
            }
        }
    }
}

int main() {
    //加快读入
    cin.tie(0), ios::sync_with_stdio(false);

    //原始输入是用的一个字符一个空格的形式,无法使用string进行读入
    char c;
    for (int i = 0; i < 9; i++) cin >> c, st += c;

    //八数码定理:检查逆序对数量
    int nums = 0;
    for (int i = 0; i < 9; i++) {
        if (st[i] == 'x') continue; //保证不是x
        for (int j = i + 1; j < 9; j++) {
            if (st[j] == 'x') continue; //保证不是x
            if (st[j] < st[i]) nums++;  //逆序数
        }
    }
    //如果逆序对数量是奇数个,则输出-1
    if (nums & 1)
        puts("unsolvable");
    else //否则通过bfs输出路径
        bfs();

    return 0;
}

### 五、双向$bfs$+无路径

#include <bits/stdc++.h>
using namespace std;
typedef unordered_map<string, int> MSI;
string st, ed = "12345678x";

int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};

// AC掉AcWing 845, 运行时间:1440 ms
int extend(queue<string> &qa, MSI &da, MSI &db) {
    string t = qa.front();
    qa.pop();

    int k = t.find('x');
    int tx = k / 3, ty = k % 3;

    for (int i = 0; i < 4; i++) {
        int x = tx + dx[i], y = ty + dy[i];
        if (x < 0 || x > 2 || y < 0 || y > 2) continue;
        string v = t;
        swap(v[k], v[x * 3 + y]);
        if (da[v]) continue;
        if (db[v]) return da[t] + 1 + db[v];
        da[v] = da[t] + 1;
        qa.push(v);
    }
    return -1;
}

int bfs() {
    //不加特判挂一个点 
    if (st == ed) return 0;

    //双队列
    queue<string> qa, qb;
    //双距离
    MSI da, db;

    //将起点放入队列
    qa.push(st);
    da[st] = 0;

    //将终点放入队列
    qb.push(ed);
    db[ed] = 0;

    int t;
    //两个队列均不空
    while (qa.size() && qb.size()) {
        //双队列优化策略:哪个小就优先拓展哪个
        if (qa.size() <= qb.size())
            t = extend(qa, da, db);
        else
            t = extend(qb, db, da);
        if (t > 0) break;
    }
    return t; //返回最短距离
}

int main() {
    //加快读入
    cin.tie(0), ios::sync_with_stdio(false);

    char c;
    for (int i = 0; i < 9; i++) cin >> c, st += c;

    //八数码定理
    int nums = 0;
    for (int i = 0; i < 9; i++) {
        if (st[i] == 'x') continue;
        for (int j = i + 1; j < 9; j++) {
            if (st[j] == 'x') continue;
            if (st[j] < st[i]) nums++;
        }
    }

    //奇数个逆序对输出-1
    if (nums & 1)
        puts("-1");
    else
        //输出最短距离
        printf("%d\n", bfs());

    return 0;
}

### 六、双向$bfs$+无路径

#include <bits/stdc++.h>

using namespace std;

const int N = 5;

// 当前串,哪个操作,前序串
typedef unordered_map<string, pair<char, string>> MSP;
//  字符串,步数
typedef unordered_map<string, int> MSI;

//操作符,上下左右
char op[] = {'u', 'd', 'l', 'r'};
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

//记录前驱路径
MSP aPre, bPre;
//记录距离
MSI da, db;

string mid;           //两个队列互相查看着搜索,当在对方HASH表中命中时,最终的那个两边都有的中间状态是什么样的字符串
queue<string> qa, qb; //两个队列,分别用于存放从起点走出来的字符串和从终点走出来的字符串

int extend(queue<string> &q, MSI &da, MSI &db, MSP &aPre, MSP &bPre) { // bPre在下面代码中未用到,但为了保持对称性,保留了这个参数
    string t = q.front();
    q.pop();

    for (int i = 0; i < 4; i++) {
        int k = t.find('x');        //在字符串t中查找x的索引位置
        int tx = k / 3, ty = k % 3; //映射的二维数组中的行与列

        int x = tx + dx[i], y = ty + dy[i];               //目标位置
        if (x < 0 || x >= 3 || y < 0 || y >= 3) continue; //出界
        string u = t;
        swap(u[x * 3 + y], u[k]); // x与目标位置交换一下

        if (da[u]) continue;  //如果搜索过
        aPre[u] = {op[i], t}; //没有搜索过时,一定要马上记录它的前驱!!!不能因为它还没有进入队列就不先记录!!!
        // 原因:因为两段式搜索,最终要输出完整的路径,否则就会出现中间缺一条线的情况,比如 ○→○→○ ←(这是这个箭头) ○←○←○,

        if (db[u]) {                  //如果对方已经搜到了
            mid = u;                  //将中间态保存到全局变量中,方便以后的操作
            return da[u] + db[u] - 1; //返回中间点距离起点、终点距离和-1
        }

        da[u] = da[t] + 1; //距离增加1
        q.push(u);
    }
    return -1; //如果本次扩展没有找到连接前后的字符串,那就返回-1表示还需要继续找
}

//出发状态,目标状态
string st, ed = "12345678x";

void bfs() {
    qa.push(st);
    da[st] = 0;

    qb.push(ed);
    db[ed] = 0;

    while (qa.size() && qb.size()) {
        int t;
        if (qa.size() <= qb.size())       //这里面是一个双向bfs的优化策略,两个队列谁小就谁使劲跑
            t = extend(qa, da, db, aPre, bPre); //从a中取状态进行扩展
        else
            t = extend(qb, db, da, bPre, aPre);
        if (t > 0) break;
    }
}

int main() {
    //加快读入
    cin.tie(0), ios::sync_with_stdio(false);

    char c;
    for (int i = 1; i <= 9; i++) cin >> c, st += c;

    //八数码定理:检查逆序对数量
    int nums = 0;
    for (int i = 0; i < 9; i++) {
        if (st[i] == 'x') continue; //保证不是x
        for (int j = i + 1; j < 9; j++) {
            if (st[j] == 'x') continue; //保证不是x
            if (st[j] < st[i]) nums++;  //逆序数
        }
    }
    //如果逆序对数量是奇数个,则输出-1
    if (nums & 1)
        puts("unsolvable");
    else {
        //双向宽搜
        bfs();

        //输出路径
        string res;
        string t = mid;
        while (t != st) {
            res += aPre[t].first;
            t = aPre[t].second;
        }
        reverse(res.begin(), res.end());

        t = mid;
        while (t != ed) {
            char cmd = bPre[t].first;
            if (cmd == 'u' || cmd == 'd') cmd = 'u' + 'd' - cmd;
            if (cmd == 'l' || cmd == 'r') cmd = 'l' + 'r' - cmd;
            res += cmd;
            t = bPre[t].second;
        }
        //输出
        cout << res << endl;
    }
    return 0;
}

### 七、$A*$寻路+无路径

$A*$ 最重要 的是 估值函数,本题中使用的估值函数办法是 曼哈顿距离:

估计距离可以理解为理想距离,举个栗子:

现实         理想 \large 1~ 5 ~4       \large 1~2~3 \large 3~ 7 ~6       \large 4~5~6 \large 8~ x ~2       \large 7~8~x

现实中$4$要走到理想位置的话,先要向左走两步,再向下一步; 现实中$8$则要向右走一步到达; 这样理想步数即为横纵坐标差距绝对值之和

现位置$(x_1,y_1)$到目标位置(x_2,y_2):

\large 曼哈顿距离 = |x_1-x_2|+|y_1-y_2|
#include <bits/stdc++.h>
using namespace std;
typedef unordered_map<string, int> MSI;

string st, ed = "12345678x";

//上右下左
int dx[] = {-1, 0, 1, 0};
int dy[] = {0, 1, 0, -1};

//伟大的结构体放光芒
struct Node {
    int dist;
    string str;
    bool operator<(const Node &t) const {
        return dist > t.dist; //距离大的反而小,距离小的反而大。所以,配合优先队列(大顶堆),小的在上,大的在下
    }
};

//估值函数,用曼哈顿距离估值
int f(string str) {
    int res = 0;
    for (int i = 0; i < str.size(); i++)
        if (str[i] != 'x') {
            int t = str[i] - '1';
            res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
        }
    return res;
}

int astar() {
    MSI dist;
    priority_queue<Node> q;

    //将估价函数与字符串形成数对,放入到优先队列中
    q.push({f(st), st});
    dist[st] = 0; //记录距离为0

    while (q.size()) {
        string t = q.top().str;
        q.pop();

        if (t == ed) break;

        int k = t.find('x');
        int tx = k / 3, ty = k % 3;
        for (int i = 0; i < 4; i++) {
            int x = tx + dx[i], y = ty + dy[i];
            if (x < 0 || x >= 3 || y < 0 || y >= 3) continue; //出界
            string v = t;                                     //将t字符串复制出来一个,生成 v字符串
            swap(v[k], v[x * 3 + y]);                         //将原字符串中x与目标位置进行交换,生成新的目标状态字符串v

            if (dist[v]) continue;
            dist[v] = dist[t] + 1;       // 更新最小值
            q.push({dist[v] + f(v), v}); // 将新的估价函数计算更新,重新入队列
        }
    }
    //返回结果
    return dist[ed];
}

int main() {
    char c;
    for (int i = 0; i < 9; i++) cin >> c, st += c;

    //八数码定理
    int nums = 0;
    for (int i = 0; i < 9; i++) {
        if (st[i] == 'x') continue;
        for (int j = i + 1; j < 9; j++) {
            if (st[j] == 'x') continue;
            if (st[j] < st[i]) nums++; //逆序数
        }
    }
    //奇数个逆序对,无解
    if (nums & 1)
        puts("-1");
    else //运行astar算法,返回最短距离
        printf("%d\n", astar());

    return 0;
}

### 八、$A*$寻路+有路径

#include <bits/stdc++.h>
using namespace std;

typedef unordered_map<string, int> MSI;
typedef unordered_map<string, pair<char, string>> MSP;

string st, ed = "12345678x";

//上下左右
char op[] = {'u', 'd', 'l', 'r'};
int dx[] = {-1, 1, 0, 0};
int dy[] = {0, 0, -1, 1};

//前驱
MSP pre;

//伟大的结构体放光芒
struct Node {
    int dist;
    string str;
    bool operator<(const Node &t) const {
        return dist > t.dist; //距离大的反而小,距离小的反而大。所以,配合优先队列(大顶堆),小的在上,大的在下
    }
};

//估值函数,用曼哈顿距离估值
int f(string str) {
    int res = 0;
    for (int i = 0; i < str.size(); i++)
        if (str[i] != 'x') {
            int t = str[i] - '1';
            res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
        }
    return res;
}

string astar() {
    MSI dist;
    priority_queue<Node> q;

    //将估价函数与字符串形成数对,放入到优先队列中
    q.push({f(st), st});
    dist[st] = 0; //记录距离为0

    while (q.size()) {
        string t = q.top().str;
        q.pop();

        if (t == ed) break;

        int k = t.find('x');
        int tx = k / 3, ty = k % 3;
        for (int i = 0; i < 4; i++) {
            int x = tx + dx[i], y = ty + dy[i];
            if (x < 0 || x >= 3 || y < 0 || y >= 3) continue; //出界
            string v = t;                                     //将t字符串复制出来一个,生成 v字符串
            swap(v[k], v[x * 3 + y]);                         //将原字符串中x与目标位置进行交换,生成新的目标状态字符串v

            if (dist[v]) continue;
            dist[v] = dist[t] + 1;       // 更新最小值
            q.push({dist[v] + f(v), v}); // 将新的估价函数计算更新,重新入队列
            pre[v] = {op[i], t};
        }
    }

    //将路径倒转过来
    string res;
    while (ed != st) {
        res += pre[ed].first;
        ed = pre[ed].second;
    }
    reverse(res.begin(), res.end());
    return res;
}

int main() {
    char c;
    for (int i = 0; i < 9; i++) cin >> c, st += c;

    //八数码定理
    int nums = 0;
    for (int i = 0; i < 9; i++) {
        if (st[i] == 'x') continue;
        for (int j = i + 1; j < 9; j++) {
            if (st[j] == 'x') continue;
            if (st[j] < st[i]) nums++; //逆序数
        }
    }
    //奇数个逆序对,无解
    if (nums & 1)
        puts("unsolvable");
    else //运行astar算法,返回最短距离
        cout << astar() << endl;

    return 0;
}