【ACWing】179. 八数码

题目地址:

https://www.acwing.com/problem/content/181/

在一个 3 × 3 3×3 3×3的网格中, 1 ∼ 8 1∼8 18 8 8 8个数字和一个X恰好不重不漏地分布在这 3 × 3 3×3 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 3×3 3×3的初始网格描绘出来。例如,如果初始网格如下所示:

1 2 3 
x 4 6 
7 5 8

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

输出格式:
输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。如果答案不唯一,输出任意一种合法方案即可。如果不存在解决方案,则输出unsolvable

可以用A*算法(需要注意这个算法只对非负权图有效)。这个算法很像Dijkstra算法,也是用一个最小堆来做BFS(也可以叫PFS,即Priority First Search),只有细微的区别。这个算法需要对每个隐式图的节点定义一个启发函数 h h h,设 d ( x ) d(x) d(x)表示出发点与 x x x的实际路径距离(这里的“实际路径”指的是通过BFS一步步扩展到 x x x的路径),而 f ( x ) f(x) f(x)表示从 x x x到终点的实际最短距离,启发函数 h h h一定要满足 ∀ x , 0 ≤ h ( x ) ≤ f ( x ) \forall x, 0\le h(x)\le f(x) x,0h(x)f(x)。在做PFS的时候,模仿Dijkstra算法一样做,堆里存的是一个pair,第一维是估价函数,其定义是 d ( x ) + h ( x ) d(x)+h(x) d(x)+h(x),第二维是走到的状态,记 ( a , x ) = ( d ( x ) + h ( x ) , x ) (a,x)=(d(x)+h(x),x) (a,x)=(d(x)+h(x),x),并且每个顶点按照 d ( x ) + h ( x ) d(x)+h(x) d(x)+h(x)小者优先出堆。当终点第一次出堆的时候就可以判定这个 a a a存的就是从起点出发到终点的实际最短距离(但是需要注意,除了终点以外,别的顶点出堆的时候无法判断对应的 a a a是否是最短路长度,这是与Dijkstra算法不同的一点。所以在A*算法里顶点出堆的时候不能标记其为“已计算出”。此外对于终点 e e e来说, h ( e ) = 0 h(e)=0 h(e)=0,所以实际上 a = d ( x ) a=d(x) a=d(x))。具体 h ( x ) h(x) h(x)的选取,需要靠经验,其取得越大,算法的效果越好。当 h ( x ) = 0 h(x)=0 h(x)=0的时候,算法就退化为Dijkstra算法(由于不能标记出堆点,所以实际比Dijkstra算法差很多);但是当 h ( x ) h(x) h(x)取的比较大的时候,A*算法是效率比较高的。此外,如果堆空了,则说明路径不存在,这一点和Dijkstra算法是一样的。

A*算法简要证明:
我们只需证明在终点第一次出堆的时候,其pair的第一维就是起点到终点的最短路。首先当终点状态 e e e出堆的时候,第一维的 h ( e ) = 0 h(e)=0 h(e)=0,所以此时的 d ( e ) d(e) d(e)存的确实是某条路径的长度。如果这个路径长度不是最短路径长度,那么其一定会比真实最短路径长度严格大。此时分两种情况考虑:
1、如果此时堆空,此时说明从起点到终点的路径只有一条(因为没有别的顶点能扩展出终点。这里严格的说,应该是简单路径只有一条,即无环的,不绕远路的路径)。只有一条的话,得到的距离肯定是最短路的距离了;
2、如果堆不空,如果当前得到的路径长度不是最短路距离,那么其必然严格大于实际最短路距离,而堆里的 d + h d+h d+h的值都是小于等于实际最短路距离的,并且真实最短路径上一定存在某个点 y y y依然在堆里(这是因为如果当前得到的路径长度不是最短路距离的话,但是它确实又是某条路径的距离,所以就说明到终点的路至少有两条,而另一条即真实最短路的那一条还没扩展到终点),这样会导致出堆的那个pair事实上不应该出堆(因为它们的第一维是大于实际最短路距离的,也就大于堆内元素 y y y所在pair的第一维)。这就矛盾了。
所以终点第一次出堆的时候对应的距离就是最短路距离。

对于八数码问题,其有解有个充分必要条件:有解当且仅当将每行的数按次序排列成一行后,逆序对的个数是偶数(准确的说是和终点状态的逆序对数奇偶性一样。本题终点状态的逆序对数是 0 0 0,是个偶数)。必要性比较好证,只需注意,当X在行内移动时,逆序对数是不变的;而X上下移动的时候,逆序对数要么不变,要么 + 2 +2 +2或者 − 2 -2 2,即奇偶性也不变。充分性证明略。此外, h ( x ) h(x) h(x)的取值是, x x x这个状态和终点状态的相同数字的曼哈顿距离之和(每个数字要走到其应该在的地方的步数最小值就是曼哈顿距离,而每个数字在挪动的时候,其余数字的曼哈顿距离是不会变的)。具体算法如下:
1、开个最小堆,push进一个pair,即 ( h ( s ) , s ) (h(s),s) (h(s),s),其中 s s s表示初始状态。对于初始状态, d ( s ) = 0 d(s)=0 d(s)=0,所以pair的第一维是 d ( s ) + h ( s ) = 0 + h ( s ) = h ( s ) d(s)+h(s)=0+h(s)=h(s) d(s)+h(s)=0+h(s)=h(s)。第二维存第一维的 d d d对应的那个状态,这样当终点状态第一次出堆的时候,第一维存的就是实际从起点到终点的最短距离;
2、开一个哈希表 d d d,存每个状态离起点的最短距离(所以一开始要存 d [ s ] = 0 d[s]=0 d[s]=0,表示初始状态自己到自己的距离是 0 0 0),同时为了存路径,再开个哈希表prev,存当前状态的前一个状态是谁,并且是怎么转移到当前状态的。
3、接着只要堆不空就进行循环,每次pop出一个pair,将这个状态 t t t能转移出的所有状态 x x x求出来,如果 d ( x ) d(x) d(x)还没求出来,或者 d ( x ) > d ( t ) + 1 d(x)>d(t)+1 d(x)>d(t)+1,那么就更新 d ( x ) = d ( t ) + 1 d(x)=d(t)+1 d(x)=d(t)+1,并且把 ( d ( x ) + h ( x ) , x ) (d(x)+h(x),x) (d(x)+h(x),x)这个pair入堆。当然如果pop的时候 t t t就已经等于终点状态了,那么就可以退出循环了,此时 d ( t ) d(t) d(t)就是最短距离,prev里就存了路径。

代码如下:

#include <iostream>
#include <algorithm>
#include <unordered_map>
#include <queue>

#define x first
#define y second

using namespace std;

typedef pair<int, string> PIS;

const int d[] = {
    
    1, 0, -1, 0, 1};
const char op[] = "dlur";
string ed = "12345678x";

// 写一下启发函数
int h(string s) {
    
    
    int res = 0;
    for (int i = 0; i < s.size(); i++) 
        if (s[i] != 'x') {
    
    
        	// t是s[i]这个数在终点状态里的下标
            int t = s[i] - '1';
            res += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
        }

    return res;
}

string bfs(string st) {
    
    
	// dist存从起点到key这个状态的最短路径的长度
    unordered_map<string, int> dist;
    unordered_map<string, pair<char, string> > prev;
    priority_queue<PIS, vector<PIS>, greater<PIS> > heap;
	
	// 将初始状态的距离初始化一下,并入堆
    dist[st] = 0;
    heap.push({
    
    h(st), st});

    while (!heap.empty()) {
    
    
        auto t = heap.top();
        heap.pop();

        string cur = t.y;
        if (cur == ed) break;

        int x, y;
        // 找一下'x'的位置
        for (int i = 0; i < 9; i++)
            if (cur[i] == 'x') {
    
    
                x = i / 3, y = i % 3;
                break;
            }

        string orig = cur;
        for (int i = 0; i < 4; i++) {
    
    
            int nx = x + d[i], ny = y + d[i + 1];
            if (0 <= nx && nx < 3 && 0 <= ny && ny < 3) {
    
    
                cur = orig;
                swap(cur[3 * x + y], cur[3 * nx + ny]);
                // 如果cur的dist没求出过,或者距离可以被更新,那么就更新并入堆
                if (!dist.count(cur) || dist[cur] > dist[orig] + 1) {
    
    
                    dist[cur] = dist[orig] + 1;
                    prev[cur] = {
    
    op[i], orig};
                    heap.push({
    
    dist[cur] + h(cur), cur});
                }
            }
        }
    }

    string res;
    while (ed != st) {
    
    
        res += prev[ed].x;
        ed = prev[ed].y;
    }

    reverse(res.begin(), res.end());
    return res;
}

int main() {
    
    
    string st, seq;
    char c;
    while (cin >> c) {
    
    
        st += c;
        if (c != 'x') seq += c;
    }

    int cnt = 0;
    for (int i = 0; i < 8; i++)
        for (int j = i + 1; j < 8; j++)
            if (seq[i] > seq[j]) cnt++;

    if (cnt & 1) cout << "unsolvable" << endl;
    else cout << bfs(st) << endl;

    return 0;
}

时间复杂度 O ( E log ⁡ V ) O(E\log V) O(ElogV),空间 O ( V ) O(V) O(V)

猜你喜欢

转载自blog.csdn.net/qq_46105170/article/details/114877098