The 15-puzzle has been around for over 100 years; even if you don't know it by that name, you've seen it. It is constructed with 15 sliding tiles, each with a number from 1 to 15 on it, and all packed into a 4 by 4 frame with one tile missing. Let's call the missing tile 'x'; the object of the puzzle is to arrange the tiles so that they are ordered as:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 x
where the only legal operation is to exchange 'x' with one of the tiles with which it shares an edge. As an example, the following sequence of moves solves a slightly scrambled puzzle:
1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4 5 6 7 8 5 6 7 8 5 6 7 8 5 6 7 8 9 x 10 12 9 10 x 12 9 10 11 12 9 10 11 12 13 14 11 15 13 14 11 15 13 14 x 15 13 14 15 x r-> d-> r->
The letters in the previous row indicate which neighbor of the 'x' tile is swapped with the 'x' tile at each step; legal values are 'r','l','u' and 'd', for right, left, up, and down, respectively.
Not all puzzles can be solved; in 1870, a man named Sam Loyd was famous for distributing an unsolvable version of the puzzle, and
frustrating many people. In fact, all you have to do to make a regular puzzle into an unsolvable one is to swap two tiles (not counting the missing 'x' tile, of course).
In this problem, you will write a program for solving the less well-known 8-puzzle, composed of tiles on a three by three
arrangement.
Input
You will receive, several descriptions of configuration of the 8 puzzle. One description is just a list of the tiles in their initial positions, with the rows listed from top to bottom, and the tiles listed from left to right within a row, where the tiles are represented by numbers 1 to 8, plus 'x'. For example, this puzzle
1 2 3
x 4 6
7 5 8
is described by this list:
1 2 3 x 4 6 7 5 8
Output
You will print to standard output either the word ``unsolvable'', if the puzzle has no solution, or a string consisting entirely of the letters 'r', 'l', 'u' and 'd' that describes a series of moves that produce a solution. The string should include no spaces and start at the beginning of the line. Do not print a blank line between cases.
Sample
Inputcopy | Outputcopy |
---|---|
2 3 4 1 5 x 7 6 8 | ullddrurdllurdruldr |
翻译
15拼图已经存在超过100年了,即使你不知道它的名字,你也一定见过它。它由15个滑动瓷砖构成,每个瓷砖上都有一个从1到15的数字,并且所有的瓷砖都紧密地装在一个4x4的框架中,有一个瓷砖缺失。我们称缺失的瓷砖为'x';这个拼图的目标是将瓷砖排列成如下顺序:
1 2 3 4
5 6 7 8
9 10 11 12
13 14 15 x
其中唯一合法的操作是将'x'与它相邻的瓷砖交换。例如,以下序列的移动解决了一个稍微打乱的拼图:
1 2 3 4 1 2 3 4 1 2 3 4 1 2 3 4
5 6 7 8 5 6 7 8 5 6 7 8 5 6 7 8
9 x 10 12 9 10 x 12 9 10 11 12 9 10 11 12
13 14 11 15 13 14 11 15 13 14 x 15 13 14 15 x
r-> d-> r->
上面一行中的字母表示每一步交换'x'瓷砖的相邻瓷砖;合法的值有'r'、'l'、'u'和'd',分别表示向右、向左、向上和向下。
并不是所有的拼图都能被解决;在1870年,一个名叫Sam Loyd的人因发布一个不可解的拼图而出名,并且让很多人感到沮丧。实际上,要将一个正常的拼图变成一个不可解的拼图,只需要交换两个瓷砖(不包括缺失的'x'瓷砖)。
在这个问题中,你将编写一个程序来解决较不知名的8拼图,它由一个三行三列的瓷砖组成。
输入
你将收到若干个8拼图的配置描述。一个描述只是一个瓷砖在初始位置的列表,其中行从上到下列出,瓷砖在行内从左到右列出,其中瓷砖用数字1到8和'x'表示。例如,这个拼图:
1 2 3
x 4 6
7 5 8
可以描述为这个列表:
1 2 3 x 4 6 7 5 8
输出
你将打印到标准输出要么是“unsolvable”,如果拼图无解;要么是一个完全由字母'r'、'l'、'u'和'd'组成的字符串,描述了一系列产生解的移动。字符串不应包含空格,并且从行首开始。不要在案例之间打印空行。
样例
Inputcopy | Outputcopy |
---|---|
2 3 4 1 5 x 7 6 8 | ullddrurdllurdruldr |
#include <string>
#include <iostream>
#include <cstdio>
#include <queue>
#include <vector>
#include <cstring> // 包含 C 风格字符串处理
#include <algorithm> // 包含常用算法
using namespace std; // 使用标准命名空间
const int maxn = 5e5 + 10; // 定义最大状态数量
int vis[maxn]; // 定义正向搜索访问标记数组
int vis2[maxn]; // 定义反向搜索访问标记数组
char d[10] = { "udlr" }, d2[10] = { "durl" }; // 定义移动方向字符数组
int dir[4] = { -3, 3, -1, 1 }; // 定义方向对应的索引变化量
int ha[9] = { 40320, 5040, 720, 120, 24, 6, 2, 1, 1 }; // 阶乘预处理数组,用于计算哈希值
string a, b = "123456780"; // 定义初始状态和目标状态字符串
// 记录节点信息的结构体
struct node {
int num; // 当前节点编号
char ch; // 从父节点到当前节点的移动方向
} pre[maxn]; // 定义节点信息数组
// 用于存储状态及其编号的结构体
struct node2 {
int num; // '0'所在的位置
string s; // 状态字符串
} e; // 定义初始状态结构体
// 递归输出解法路径
void show(int x) {
if (pre[x].num == -1) // 如果是初始节点,返回
return;
show(pre[x].num); // 递归输出前一个节点
printf("%c", pre[x].ch); // 输出当前节点的移动方向
}
// 计算状态的哈希值
int getha(string s) {
int sum = 0; // 初始化哈希值
for (int i = 0; i < 9; i++) { // 遍历字符串
int k = 0; // 逆序数计数
for (int j = i + 1; j < 9; j++) { // 计算逆序数
if (s[j] < s[i]) k++;
}
sum += k * ha[i]; // 计算哈希值
}
return sum; // 返回哈希值
}
// 广度优先搜索,包含双向搜索
void bfs() {
int q = getha(e.s); // 计算初始状态的哈希值
queue<node2> q1; // 定义正向搜索队列
queue<node2> q2; // 定义反向搜索队列
vis[q] = 1; // 标记初始状态已访问
node2 f, g; // 定义状态结构体
f.s = b; // 设置目标状态
f.num = 8; // 设置'0'的位置
int p = getha(f.s); // 计算目标状态的哈希值
int x, k; // 定义临时变量
vis2[p] = 2; // 标记目标状态已访问
pre[1].num = -1; // 初始化路径记录
pre[2].num = -1; // 初始化路径记录
int num = 2; // 设置状态编号初始值
q1.push(e); // 初始状态入队
q2.push(f); // 目标状态入队
while (!q1.empty() && !q2.empty()) { // 如果两个队列都不为空
f = q1.front(); // 取出正向搜索队列的队首元素
q1.pop(); // 弹出队首元素
p = getha(f.s); // 计算当前状态的哈希值
if (vis2[p]) { // 如果反向搜索已访问
show(vis[p]); // 输出正向路径
k = vis2[p]; // 取出反向路径起点
while (pre[k].num != -1) { // 输出反向路径
printf("%c", pre[k].ch); // 输出方向字符
k = pre[k].num; // 取下一个节点
}
printf("\n"); // 输出换行
return; // 结束搜索
}
for (int i = 0; i < 4; i++) { // 遍历四个方向
if (i == 0 && f.num < 3) continue; // 如果不能向上移动,跳过
if (i == 1 && f.num > 5) continue; // 如果不能向下移动,跳过
if (i == 2 && f.num % 3 == 0) continue; // 如果不能向左移动,跳过
if (i == 3 && f.num % 3 == 2) continue; // 如果不能向右移动,跳过
x = f.num + dir[i]; // 计算移动后的'0'位置
g = f; // 复制当前状态
swap(g.s[f.num], g.s[x]); // 交换'0'与目标位置
q = getha(g.s); // 计算新状态的哈希值
if (vis[q]) continue; // 如果新状态已访问,跳过
vis[q] = ++num; // 标记新状态已访问,并记录编号
g.num = x; // 更新'0'的位置
pre[num].num = vis[p]; // 记录路径信息
pre[num].ch = d[i]; // 记录移动方向
q1.push(g); // 新状态入队
}
f = q2.front(); // 取出反向搜索队列的队首元素
q2.pop(); // 弹出队首元素
p = getha(f.s); // 计算当前状态的哈希值
if (vis[p]) { // 如果正向搜索已访问
show(vis[p]); // 输出正向路径
k = vis2[p]; // 取出反向路径起点
while (pre[k].num != -1) { // 输出反向路径
printf("%c", pre[k].ch); // 输出方向字符
k = pre[k].num; // 取下一个节点
}
printf("\n"); // 输出换行
return; // 结束搜索
}
for (int i = 0; i < 4; i++) { // 遍历四个方向
if (i == 0 && f.num < 3) continue; // 如果不能向上移动,跳过
if (i == 1 && f.num > 5) continue; // 如果不能向下移动,跳过
if (i == 2 && f.num % 3 == 0) continue; // 如果不能向左移动,跳过
if (i == 3 && f.num % 3 == 2) continue; // 如果不能向右移动,跳过
x = f.num + dir[i]; // 计算移动后的'0'位置
g = f; // 复制当前状态
swap(g.s[f.num], g.s[x]); // 交换'0'与目标位置
q = getha(g.s); // 计算新状态的哈希值
if (vis2[q]) continue; // 如果新状态已访问,跳过
vis2[q] = ++num; // 标记新状态已访问,并记录编号
g.num = x; // 更新'0'的位置
pre[num].num = vis2[p]; // 记录路径信息
pre[num].ch = d2[i]; // 记录移动方向
q2.push(g); // 新状态入队
}
}
printf("unsolvable\n"); // 如果队列为空,输出 unsolvable
}
int main() {
while (getline(cin, a)) { // 读取输入行
int n, k = 0; // 定义字符串长度和逆序数计数器
n = a.size(); // 获取输入字符串长度
e.s = ""; // 初始化状态字符串
for (int i = 0, j = 0; i < n; i++) { // 遍历输入字符串
if (a[i] != ' ') { // 如果不是空格
if (a[i] == 'x') { // 如果是'x'
e.num = j; // 记录'0'的位置
e.s += '0'; // 将'x'转换为'0'
} else {
e.s += a[i]; // 添加字符到状态字符串
}
j++; // 字符计数器加1
}
}
for (int i = 0; i < 9; i++) { // 遍历状态字符串
if (e.s[i] == '0') continue; // 如果是'0'跳过
for (int j = 0; j < i; j++) { // 计算逆序数
if (e.s[j] == '0') continue; // 如果是'0'跳过
if (e.s[j] > e.s[i]) k++; // 如果前面的数比当前数大,逆序数加1
}
}
memset(vis2, 0, sizeof(vis2)); // 重置反向访问标记数组
memset(vis, 0, sizeof(vis)); // 重置正向访问标记数组
if (k & 1) { // 如果逆序数为奇数
printf("unsolvable\n"); // 输出 unsolvable
} else {
bfs(); // 否则进行广度优先搜索
}
}
return 0; // 程序结束
}