You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18 KiB

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

AcWing 179 八数码

题目传送门

一、题目描述

在一个 3×3 的网格中,188 个数字和一个 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 与上下左右方向数字交换的行动记录为 udlr

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

输入格式 输入占一行,将 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;
}