## $AcWing$ $179$ 八数码 [题目传送门](https://www.acwing.com/problem/content/181/) ### 一、题目描述 在一个 $3×3$ 的网格中,$1$∼$8$ 这 $8$ 个数字和一个 $X$ 恰好不重不漏地分布在这 $3×3$ 的网格中。 例如: ```c++ 1 2 3 X 4 6 7 5 8 ``` 在游戏过程中,可以把 $X$ 与其上、下、左、右四个方向之一的数字交换(如果存在)。 我们的目的是通过交换,使得网格变为如下排列(称为正确排列): ```c++ 1 2 3 4 5 6 7 8 X ``` 例如,示例中图形就可以通过让 $X$ 先后与右、下、右三个方向的数字交换成功得到正确排列。 交换过程如下: ```c++ 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$ 的初始网格描绘出来。 例如,如果初始网格如下所示: ```c++ 1 2 3 x 4 6 7 5 8 ``` 则输入为:`1 2 3 x 4 6 7 5 8` **输出格式** 输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。 如果答案不唯一,输出任意一种合法方案即可。 如果不存在解决方案,则输出 `unsolvable`。 ### 二、八数码定理 * 当初始状态棋局的棋子数列的 **逆序数** 是 **奇数** 时,八数码问题 **无解** * 当初始状态棋局的棋子数列的 **逆序数** 是 **偶数** 时,八数码问题 **有解** ### 三、朴素版本$bfs$+无路径 ```c++ #include using namespace std; typedef unordered_map MSI; //无脑BFS解法 string st, ed = "12345678x"; //上右下左 四个方向 int dx[] = {-1, 0, 1, 0}; int dy[] = {0, 1, 0, -1}; int bfs() { queue 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$+有路径 ```c++ #include using namespace std; typedef unordered_map MSI; typedef unordered_map> 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 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$+无路径 ```c++ #include using namespace std; typedef unordered_map 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 &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 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$+无路径 ```c++ #include using namespace std; const int N = 5; // 当前串,哪个操作,前序串 typedef unordered_map> MSP; // 字符串,步数 typedef unordered_map 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 qa, qb; //两个队列,分别用于存放从起点走出来的字符串和从终点走出来的字符串 int extend(queue &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|$$ ```c++ #include using namespace std; typedef unordered_map 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 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*$寻路+有路径 ```c++ #include using namespace std; typedef unordered_map MSI; typedef unordered_map> 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 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; } ```