其他分享
首页 > 其他分享> > 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;
}

标签:return,string,int,++,数码,179,字符串,操作,AcWing
来源: https://www.cnblogs.com/littlehb/p/15970388.html