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