题解 P1379 八数码难题
题目描述
在 \(3\times 3\) 的方格里面,放置了 \(1 \sim 8\) 这 \(8\) 个数字,有一个空格。
每次操作可以把空格和上下左右中的一个格子交换位置,给定一个初始状态,问让这个状态变成终止状态最少需要多少步。
状态中的空格都用 0
表示。
终止状态是:
1 2 3
8 0 4
7 6 5
Solution
虽然本题不需要判断无解,但是其实无解的条件就是把这个棋盘按照行读下来(空格跳过),逆序对的数量是奇数。
证明比较复杂,可以参考《算法竞赛进阶指南》。
考虑朴素的搜索:使用 bfs
,每次可以把当前的局面把空格向四个方向移动,然后继续搜索。
直接搜索时间开销很大,考虑优化:
-
开个
dist
数组保存每个状态的最小步数,每次仅当拓展之后的步数比原本的步数更优才拓展。 -
使用 \(A*\) ,因为估价函数要比真实的答案小,并且要尽量接近真实的答案,所以可以设成每个数字的当前位置到它的目标位置的曼哈顿距离。然后每次搜索的时候取出当前所有待考虑决策中 已走步数 \(+\) 估价函数 值最小的一个进行拓展,用优先队列解决。
基本思路就是这样。至于怎么存下一个棋盘的信息,可以用 string
横向存下来,然后用一个 map
/ unordered_map
存下每个状态的 dist
。
代码如下:
#include <iostream>
#include <algorithm>
#include <string>
#include <queue>
#include <cmath>
#include <unordered_map>
using namespace std;
char g[4][4];
inline void put(string s) {
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
g[i][j] = s[(i - 1) * 3 + j - 1];
}
inline string get() {
string s;
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++)
s += g[i][j];
return s;
}
inline int getdist(int x1 ,int y1 ,int x2 ,int y2) {
return abs(x2 - x1) + abs(y2 - y1);
}
pair <int ,int> posit[9];
inline void init(string s = "123804765") {
for (int c = 0 ,i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++ ,c++) {
if (s[c] == '0') continue;
posit[s[c] - '0'] = make_pair(i ,j);
}
}
struct node {
string now; int dis ,f;
node (string now = "" ,int dis = 0 ,int f = 0) : now(now) ,dis(dis) ,f(f) {}
inline void set() {
for (int i = 1; i <= 3; i++)
for (int j = 1; j <= 3; j++) {
if (g[i][j] == '0') continue;
int c = g[i][j] - '0';
f += getdist(i ,j ,posit[c].first ,posit[c].second);
}
}
friend bool operator < (node a ,node b) {
return a.dis + a.f > b.dis + b.f; //注意 STL 默认是大根堆,如果要定义小根堆需要反向定义
}
};
const string ed = "123804765"; //目标状态
const int dx[] = {0 ,-1 ,1 ,0 ,0} ,dy[] = {0 ,0 ,0 ,-1 ,1};
inline int Astar(string st) {
unordered_map <string ,int> distan;
priority_queue <node> q;
distan[st] = 0;
node t = node(st ,0 ,0); t.set();
if (st == ed) return 0;
q.push(t);
while (!q.empty()) {
string now = q.top().now; int dis = q.top().dis; q.pop();
if (now == ed) return dis;
put(now);
int x = 0 ,y = 0;
for (int i = 1; i <= 3; i++) //找到空格的位置
for (int j = 1; j <= 3; j++)
if (g[i][j] == '0') {
x = i ,y = j; break;
}
for (int i = 1; i <= 4; i++) {
int tx = x + dx[i] ,ty = y + dy[i];
if (tx < 1 || ty < 1 || tx > 3 || ty > 3) continue;
swap(g[x][y] ,g[tx][ty]); //移动就相当于交换。
string s = get();
if (s == ed) return dis + 1;
if (!distan.count(s)) {
t = node(s ,dis + 1 ,0); t.set();
distan[s] = dis + 1;
q.push(t);
}
swap(g[x][y] ,g[tx][ty]);
}
}
return -1;
}
string st;
signed main() {
ios :: sync_with_stdio(false);
cin >> st;
init();
cout << Astar(st) << endl;
return 0;
}