179. 八数码
在一个 3×3的网格中,1∼8这 8个数字和一个 X 恰好不重不漏地分布在这 3×3的网格中。
例如:
1 2 3
X 4 6
7 5 8
在游戏过程中,可以把 X 与其上、下、左、右四个方向之一的数字交换(如果存在)。
我们的目的是通过交换,使得网格变为如下排列(称为正确排列):
1 2 3
4 5 6
7 8 X
例如,示例中图形就可以通过让 X 先后与右、下、右三个方向的数字交换成功得到正确排列。
交换过程如下:
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的初始网格描绘出来。
例如,如果初始网格如下所示:
1 2 3
x 4 6
7 5 8
则输入为:1 2 3 x 4 6 7 5 8
输出格式
输出占一行,包含一个字符串,表示得到正确排列的完整行动记录。
如果答案不唯一,输出任意一种合法方案即可。
如果不存在解决方案,则输出 unsolvable。
输入样例:
2 3 4 1 5 x 7 6 8
输出样例
ullddrurdllurdruldr
这个题型算法分析与设计课程中讲过
难点一:如何保证有解
八数码问题有解等价于:将字符按行读取之后,读出的序列的逆序对的数量为偶数(该结论对所有n为奇数的
n
∗
n
n*n
n∗n 方格数码成立)
因为本题只能上下左右四个方向移动(将x看作空位的话)
- 其中,某个单元向右移动或者向左移动时,并不改变最后按行取出的序列数字顺序,此时序列对的改变数目为0
- 若单元格向上(向下)移动,此时该单元格在按行取出的序列中,将向前移动两个字符(向后移动两个字符)。和每个字符将形成一个奇数的序列对数目改变(要么+1,要么-1),两个奇数之和必然为偶数;
- 初始序列(正确序列)的逆序对数量为0,每次移动造成序列对偶数的改变,因此最后序列对必然为偶数
其中逆序对的定义为:在给定序列中,若 i < j i<j i<j 且有 a [ i ] > a [ j ] a[i] > a[j] a[i]>a[j],此时构成一个逆序对
难点二:
本题中的估价函数:当前状态中的每个数字与它目标状态的曼哈顿距离之和
(
x
i
,
y
i
)
和
(
x
j
,
y
j
)
(x_i,y_i)和(x_j,y_j)
(xi,yi)和(xj,yj)的曼哈顿距离:
D
(
i
,
j
)
=
∣
x
i
−
x
j
∣
+
∣
y
i
−
y
j
∣
D(i,j) = |x_i - x_j| ~+~|y_i - y_j|
D(i,j)=∣xi−xj∣ + ∣yi−yj∣
关于
A
∗
A^*
A∗算法:
可以理解为加了启发式剪枝的BFS。
启发式剪枝,是一种估算当前状态到终点状态所需的步数,我们称估算的方法叫做估价函数。设当前点为
u
u
u,已经走了
f
(
u
)
f(u)
f(u)步,估价函数为
h
(
u
)
h(u)
h(u),实际到终点的距离为
g
(
u
)
g(u)
g(u),则必须满足以下性质:
h
(
u
)
≤
g
(
u
)
h(u) \leq g(u)
h(u)≤g(u)
所以我们要以
f
(
u
)
+
h
(
u
)
f(u)+h(u)
f(u)+h(u)为关键字进行排序(有点类似于堆化的
D
i
j
k
s
t
r
a
Dijkstra
Dijkstra)
简单来说 A ∗ A^* A∗算法在处理队列的元素上进行了优化
- 拿出队头并扩展元素
- 估计扩展元素到终点的距离
- 将扩展后的元素进行从小到大的排序,并且小的作为对头(这样做大大减少了几乎无意义的数据带来的影响)
应用的环境:
- 有解(无解时,仍然会把所有空间搜索,会比一般的bfs慢,因为优先队列的操作是logn的)
- 边权非负,如果是负数,那么终点的估值有可能是负无穷,终点可能会直接出堆
性质:除了终点以外的其他点无法在出堆或者入堆的时候确定距离,只能保证终点出堆时是最优的。
关于题目本身:
自己理解的本题其实不算严格意义上的按层来拓展,尤其是代码的第55行。在之前的题目中通常会设置一个vis数组或者直接通过map的count方法来判定某个元素是否被访问过,而如果被访问过通常会直接continue,因为如果按层次遍历则第一次被遍历到的一定是到源点路径最短的
而本题的拓展规则是要与目标序列靠齐,为了减小搜索宽度,本题添加了一个估价函数,用曼哈顿距离来刻画当前序列和目标序列的相似程度,并且先搜索与目标序列相似度最高的序列
所以本题不算从源点开始的不断广搜。而是每次从队首取出与目标序列相似度最高的序列,进行以这个序列为元序列往下一层的广搜,并将这些序列入队;再从队首取出相似度最高的序列,再进行往下一层的广搜…所以会出现需要更新的情况。因为每次队首选出的元序列本身,它的dis就不一定的递增的(以前的广搜相当于将其与源点的距离作为第一标准放入了优先队列)
但是第一次更新到end的时候,一定是距离最短的时候,对此结论可做以下解释:
设终点出队的时候的距离为
d
i
s
dis
dis,且
d
i
s
>
d
i
s
b
e
s
t
dis>disbest
dis>disbest(最优解)
设在最优路径上存在一点
u
u
u,最优路径为起点——>
u
u
u——>终点
d
i
s
t
优
dist_{优}
dist优
=
d
i
s
t
[
u
]
+
g
(
u
)
≥
d
i
s
t
[
u
]
+
f
(
u
)
= dist[u] + g(u) \geq dist[u] + f(u)
=dist[u]+g(u)≥dist[u]+f(u)
=
>
d
i
s
t
[
e
n
d
]
>
d
i
s
t
优
>
=
d
i
s
t
[
u
]
+
f
(
u
)
~~~~~~~~~~~ => dist[end] > dist_优 >= dist[u] + f(u)
=>dist[end]>dist优>=dist[u]+f(u)
说明优先队列中存在一个比出堆元素更小的值(end已经出队作为答案而正确路径u还未出队),这就矛盾了。
所以说终点第一次出堆时就是最优的。
#include<bits/stdc++.h>
using namespace std;
//这里将第一个设置为int类型方便后续存放在小根堆中
//若优先队列中存储的为复合结构,如pair,默认情况下,排序规则是先按照pair的first的属性降序排列,如果first相等,则按照second属性降序排序
//而本题中的第一考虑元素就是first中存储的曼哈顿距离
typedef pair<int, string> PIS;
//估价函数计算,计算当前序列中的所有点和目标序列中的曼哈顿距离
int f(string state)
{
int dis = 0;
for(int i = 0;i < state.size();i ++){
if(state[i] != 'x'){
int t = state[i] - '1'; //t为当前位置的数字,i为应该在该位置的数字
dis += abs(i / 3 - t / 3) + abs(i % 3 - t % 3);
}
}
return dis;
}
string bfs(string start)
{
int dx[] = {-1, 1, 0, 0}, dy[] = {0, 0, -1, 1};
char op[] = {'u', 'd', 'l', 'r'};
string end = "12345678x";
unordered_map<string, int> dis; //用来存储距离,以及从起点开始变化的步数,每移动一次在原有的dis上++
unordered_map<string, pair<string, char> > prev; //当前序列-->前序列,到当前序列的操作字符 的一组映射
priority_queue<PIS, vector<PIS>, greater<PIS> > heap; //建立一个小根堆,以第一个PIS的第一个估价函数来优化队列
dis[start] = 0; heap.push({f(start), start}); //初始化
while(!heap.empty()){
PIS now = heap.top(); heap.pop();
string state = now.second; //记录当前拓展的元序列
int step = dis[state]; //记录到起点的距离(按层拓展)
if(state == end) break;
int x, y; //找到当前的空格点
for(int i = 0;i < state.size();i ++){
if(state[i] == 'x'){
x = i / 3;
y = i % 3;
break;
}
}
string init = state;
for(int i = 0;i < 4;i ++){
int sx = x + dx[i], sy = y + dy[i];
if(sx < 0 || sy < 0 || sx >= 3 || sy >= 3) continue;
state = init;
swap(state[x * 3 + y], state[sx *3 + sy]);
//如果当前序列没有被拓展过且其值需要更新
if(!dis.count(state) || dis[state] > step + 1){
dis[state] = step + 1;
prev[state] = {init, op[i]};
heap.push({dis[state] + f(state), state});
}
}
}
string ans = "";
while(end != start){
ans += prev[end].second;
end = prev[end].first;
}
reverse(ans.begin(), ans.end());
return ans;
}
int main()
{
string start, seq, c;
while(cin >> c){ //读取含有空格的输入
start += c;
if(c != "x") seq += c;
}
int cnt = 0; //统计逆序对的数量
for(int i = 0;i < 8;i ++){
for(int j = i + 1;j < 8;j ++){
if(seq[i] > seq[j]) cnt ++;
}
}
if(cnt % 2) puts("unsolvable");
else cout << bfs(start) << endl;
return 0;
}