1. 程式人生 > 其它 >AcWing 179 八數碼

AcWing 179 八數碼

二、雙向bfs解法

雙向bfs題解
https://www.acwing.com/solution/content/43817/

八數碼(雙向廣搜bfs解法)
https://www.bilibili.com/video/BV185411A7nG?spm_id_from=333.999.0.0

八數碼(程式碼落實詳細講解)
https://www.bilibili.com/video/BV1ib4y1D7uc?spm_id_from=333.999.0.0

#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;
const char op[] = {'u', 'd', 'l', 'r'};
// apre記錄從起來出來的路徑和轉化方式
MSP aPre, bPre;
// da 表示從起點出來的字串到起點的距離 db表示從終點出來的距離終點的距離
MSI da, db;
string mid;           //中間狀態
queue<string> qa, qb; //兩個佇列,分別用於存放從起點走出來的字串和從終點走出來的字串
char g[N][N];         //用於計算變化操作的字元陣列
int cnt;              //已經進行了的搜尋次數
//將字串轉換為字元陣列以便進行變化操作
void setStr(string s) {
    for (int i = 0; i < 3; i++)
        g[0][i] = s[i], g[1][i] = s[i + 3], g[2][i] = s[i + 6];
}
//這個是將我們變化後的陣列轉回字串進行後續操作
string getStr() {
    string res;
    for (int i = 0; i < 3; i++)
        res += g[i][0], res += g[i][1], res += g[i][2];
    return res;
}

string up(string s) { //第一種變化方式 u
    setStr(s); //先把字串轉化為陣列便於操作
    int x, y;  //用於記錄當前x,y的位置
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            if (g[i][j] == 'x' && i == 0)
                return "x"; //如果在邊界上,說明不能走,返回x表示不能走
            else if (g[i][j] == 'x' && i != 0)
                x = i, y = j; //如果可以走得話,記錄一下下標,然後操作
    swap(g[x][y], g[x - 1][y]);
    return getStr(); //返回操作後的字串
}
string down(string s) { //第二種變化方式 d
    setStr(s);          //先把字串轉化為陣列便於操作
    int x, y;           //用於記錄當前x的位置
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            if (g[i][j] == 'x' && i == 2)
                return "x"; //如果在邊界上,說明不能走,返回 x 表示不能走
            else if (g[i][j] == 'x' && i != 2)
                x = i, y = j; //如果可以走得話,記錄一下下標,然後操作
    swap(g[x][y], g[x + 1][y]);
    return getStr(); //返回操作後的字串
}
string left(string s) { //第三種變化方式 l
    setStr(s);          //先把字串轉化為陣列便於操作
    int x, y;           //用於記錄當前x的位置
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            if (g[i][j] == 'x' && j == 0)
                return "x"; //如果在邊界上,說明不能走,返回 x 表示不能走
            else if (g[i][j] == 'x' && j != 0)
                x = i, y = j; //如果可以走得話,記錄一下下標,然後操作
    swap(g[x][y], g[x][y - 1]);
    return getStr(); //返回操作後的字串
}
string right(string s) { //第四種變化方式 r
    setStr(s);           //先把字串轉化為陣列便於操作
    int x, y;            //用於記錄當前x的位置
    for (int i = 0; i < 3; i++)
        for (int j = 0; j < 3; j++)
            if (g[i][j] == 'x' && j == 2)
                return "x"; //如果在邊界上,說明不能走,返回 x 表示不能走
            else if (g[i][j] == 'x' && j != 2)
                x = i, y = j; //如果可以走得話,記錄一下下標,然後操作
    swap(g[x][y], g[x][y + 1]);
    return getStr(); //返回操作後的字串
}

//擴充套件一下隊內元素較少的那一個,能有效減少我們的演算法實際執行效率
int extend(queue<string> &q, MSI &da, MSI &db, MSP &apre, MSP &bpre) {
    for (int i = 0; i < q.size(); i++) {
        string t = q.front(); //取出對頭擴充套件
        q.pop();              //出隊

        string st[] = {up(t), down(t), left(t), right(t)}; //記錄一下我們不同操作對應的結果狀態
        for (int i = 1; i <= 4; i++) {
            string u = st[i - 1];
            //如果下一步的狀態可達,並且沒有訪問過的話,走之
            if (u != "x" && !da[u]) {
                da[u] = da[t] + 1; //距離增加1
                apre[u] = {op[i - 1], t};
                //如果當前的字串在對方中已經被找到過了,那說明二者之間已經有了一個聯絡,那就可以結束尋找了
                if (db[u]) {                  //如果對方已經搜到了
                    mid = u;                  //將中間態儲存到全域性變數中,方便以後的操作
                    return da[u] + db[u] - 1; //返回中間點距離起點、終點距離和-1
                }
                q.push(u); //放入佇列進行擴充套件
            }
        }
    }
    return -1; //如果本次擴充套件沒有找到連線前後的字串,那就返回-1表示還需要繼續找
}

int bfs(string A, string B) {
    qa.push(A); //先將起點放入我們從起點開始擴充套件的佇列,作為起點
    da[A] = 0;  //讓起點距離起點的距離設定為0

    qb.push(B); //先將終點放入我們從終點開始擴充套件的佇列,作為終點
    db[B] = 0;  //讓終點距離終點的距離設定為0

    //當二者佇列裡面同時含有元素的時候,才滿足繼續擴充套件的條件
    //當某個佇列搜尋完畢,還沒有拿到答案的話,就說明完畢的那個自已形成了閉環,
    //永遠不可能與另一個相交,肯定是沒有結果了
    while (qa.size() && qb.size()) {
        //神奇的特判操作,由於這題目的特殊性質,雙方有可能搜了很久還是沒搜到交集,
        //那麼就其實說明無解了,其實這也是搜尋的一種特殊處理辦法,值得學習
        cnt++;
        if (cnt >= 20) return -1; //這個數字是黃海試出來的~

        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 != -1) return t;
    }
    return -1; //如果到最後都沒有找到的話,那也說明無解了
}

int main() {
    //出發狀態,目標狀態
    string A, B = "12345678x";
    char x;
    for (int i = 1; i <= 9; i++) cin >> x, A += x; //生成起點

    int ans = bfs(A, B); //進行搜尋

    if (ans == -1)
        printf("unsolvable"); //如果無解
    else {                    //如果有解
        string res1, res2;    //前半段字串,後半段字串

        // A->mid的過程
        //因為每個節點記錄的是前驅,所以需要從mid開始向回來推
        //為每每個節點記錄的是前驅,而不是記錄後繼呢?因為每個節點可能最多有4個後繼,
        //沒有唯一性,而記錄前驅有唯一性。
        string t = mid;        //中間狀態
        while (t != A) {       //找出我們從起點到中間點所經歷的  操作(udlr的意思)
            res1 += aPre[t].first; //拼接出操作的符號udlr...
            t = aPre[t].second;    //向前一個狀態字串
        }
        //由於我們所得到的順序是從中間到起點的,那麼我們需要的是從起點到中間的,直接倒一邊就好了
        //注意這裡只是順序反了一下,操作是沒問題的,因為我們實際上就是從起點到終點的
        //只不過我們取出來的時候是反向取的,和下面的操作有一些差別
        reverse(res1.begin(), res1.end());

        // B->mid的過程
        /*這一步操作需要特殊說明,由於我們實際上,也就是程式碼上實現的是從終點到中間點的,但是我們需要的是
        從中間點到終點的,它的轉換其實不是一個簡單的倒回去操作就可以實現的,比如,我們在前面解釋過的例子
        我們需要把每個操作都反一下,才是回來的操作,也就是從中間點到終點的操作(前面已經解釋)
        */
        t = mid;
        while (t != B) { //找出我們從終點到中間點所經歷的  操作(udlr的意思)
            char cmd = bPre[t].first;
            if (cmd == 'u' || cmd == 'd')
                cmd = 'u' + 'd' - cmd;
            else
                cmd = 'l' + 'r' - cmd;
            res2 += cmd;
            t = bPre[t].second; //向後一個狀態字串
        }
        //為什麼後半段不需要轉化呢?前面已經解釋過了就不再贅述
        cout << res1 << res2; //最後輸出兩段合起來的就好了
    }
    return 0;
}