Description
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:
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 a description of a configuration of the 8 puzzle. The 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.
Sample Input
2 3 4 1 5 x 7 6 8
Sample Output
ullddrurdllurdruldr
分析
题意:八数码问题也称为九宫问题。在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。要求解决的问题是:给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。
一个九宫格中总共有9个数字,共有9!次即362880次排列变化,对于初始状态与目标状态确定的这题我们可以用广度优先搜索,首先要解决如何判断重的问题,如果用9位数字的话数组太大了而且浪费,首先要用种方法对这么多状态进行映射,以减少内存占用。
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。具体请看维基百科-康托展开。
搜索思路:
将起点加入数组a;
定义head, tail分别指向队列开始位置与队列尾;
while(head < tail){
//根据head指向的结点a1向周围搜索下一个可用结点a2,存储a2的信息,加入队列;
//检查搜到的结点是否是目标结点,如果是返回,不是的话将head指向a2, 用a2继续扩展。
}
实现
下面的程序虽然用样例输入输出的结果与样例输出不同,但是经试验确实可以移到到目标节点,所以这题的答案应该是不唯一的。单向广搜255ms, 双向广搜16ms左右,假设从开始结点到目标结点需要n次移动,则单向广搜搜索深度为n^2, 双向广搜为2*N^2,所以双向广搜要快的多。
###单向广搜
#include <iostream>
#include <cstring>
#include <cstdio>
//八数码问题总共9个位置,也可以求15数码之类。
#define N 9
#define R 3
#define C 3
#define FACTORIAL_N 362890 //9! + 10, 10只是习惯了防止数组越界。
#define NO_PARENT -1 //没有父节点
struct node
{
char data[N + 1]; //当前节点数字排列
int blankPos; //空格位置
int parentIndex; //父节点在数组a中的下标,可据此找到上一步的移动信息。
int d; //从父节点的哪个方向扩展而来
}a[FACTORIAL_N];
bool visited[FACTORIAL_N];
const int factorial[N] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320 }; //康托展开需要,N!,如求15数码之类需扩展此处。
int canMoveStep[N][4]; //每个数字四个方向是否可移动,四个方向走算位置变换,如空格与上方换位置数就减3。
char direction[] = "lurd";
char target[] = "123456789";
int hash(char* data)
{
int sum = 0;
for (int i = 0; i < N; i++) {
int count = 0;
for (int j = i + 1; j < N; j++) {
if (data[i] > data[j]) { //求逆序数数目,因为是只找data[i]右边的数,所以不用判断比它小的数是否已经用过。
count++;
}
}
sum += count * factorial[N - i - 1];
}
return sum;
}
inline bool getTarget(node *a)
{
return strcmp(a->data, target) == 0;
}
int bfs(node *firstNode)
{
memset(visited, 0, sizeof(visited));
int head = 0, tail = 1; //首、尾指针
strcpy(a[head].data, firstNode->data);
a[head].blankPos = firstNode->blankPos;
a[head].parentIndex = NO_PARENT;
a[head].d = -1;
if (getTarget(&a[head])) return head;
visited[hash(a[head].data)] = 1;
while (head < tail) {
int blankPos = a[head].blankPos;
for (int i = 0; i < 4; i++) {
if (canMoveStep[blankPos][i]) {
a[tail].blankPos = blankPos + canMoveStep[blankPos][i];
strcpy(a[tail].data, a[head].data); //将移动后的新结点信息放入对列中。
char temp = a[tail].data[blankPos]; //保存新结点的数字排列
a[tail].data[blankPos] = a[tail].data[a[tail].blankPos];
a[tail].data[a[tail].blankPos] = temp;
a[tail].parentIndex = head;
a[tail].d = i;
int key = hash(a[tail].data);
if (!visited[key]) {
visited[key] = 1;
if (getTarget(&a[tail])) return tail;
tail++;
}
}
}
head++; //处理下一个入列结点
}
return -1;
}
void initMoveFlags()
{
memset(canMoveStep, 0, sizeof(canMoveStep));
for (int i = 0; i < N; i++) {
canMoveStep[i][0] = i % C != 0 ? -1 : 0; //第0列不能向左搜索
canMoveStep[i][2] = (i + 1) % C != 0; //最后一列不能向右搜索
canMoveStep[i][1] = i < C ? 0 : -C; //第0行不能向上搜索
canMoveStep[i][3] = i >= (N - C) ? 0 : C; //最后一行不能向下搜索
}
}
void print(int index)
{
//有父节点才能从上一步走过来
if (a[index].parentIndex == NO_PARENT) return;
print(a[index].parentIndex);
printf("%c", direction[a[index].d]);
}
int main()
{
// freopen("in.txt", "r", stdin);
initMoveFlags();
node firstNode;
char ch[2];
for (int i = 0; i < N; i++) {
scanf("%s", ch);
if (ch[0] == 'x') {
firstNode.data[i] = N + '0';
firstNode.blankPos = i;
}
else {
firstNode.data[i] = ch[0];
}
}
firstNode.data[N] = '\0';
int resultIndex = bfs(&firstNode);
if (resultIndex == NO_PARENT) {
printf("unsolveable\n");
}
else {
print(resultIndex);
printf("\n");
}
}
###双向广搜
#include <iostream>
#include <cstring>
#include <cstdio>
//八数码问题总共9个位置,也可以求15数码之类。
#define N 9
#define R 3
#define C 3
#define FACTORIAL_N 362890 //9! + 10, 10只是习惯了防止数组越界。
#define NO_PARENT -1 //没有父节点
struct node
{
char data[N + 1]; //当前节点数字排列
int blankPos; //空格位置
int parentIndex; //父节点在数组a中的下标,可据此找到上一步的移动信息。
int d; //从父节点的哪个方向扩展而来
int key;
}a[2][FACTORIAL_N];
bool visited[2][FACTORIAL_N];
const int factorial[N] = { 1, 1, 2, 6, 24, 120, 720, 5040, 40320 }; //康托展开需要,N!,如求15数码之类需扩展此处。
int canMoveStep[N][4]; //每个数字四个方向是否可移动,四个方向走算位置变换,如空格与上方换位置数就减3。
char direction[] = "lurd";
char target[] = "123456789";
int hash(char* data)
{
int sum = 0;
for (int i = 0; i < N; i++) {
int count = 0;
for (int j = i + 1; j < N; j++) {
if (data[i] > data[j]) { //求逆序数数目,因为是只找data[i]右边的数,所以不用判断比它小的数是否已经用过。
count++;
}
}
sum += count * factorial[N - i - 1];
}
return sum;
}
inline bool getTarget(node *a)
{
return strcmp(a->data, target) == 0;
}
int dbfs(node *firstNode)
{
int key;
memset(visited, 0, sizeof(visited));
int head[] = { 0, 0 }, tail[] = { 1, 1 }; //首、尾指针
strcpy(a[0][head[0]].data, firstNode->data);
a[0][head[0]].blankPos = firstNode->blankPos;
a[0][head[0]].parentIndex = NO_PARENT;
a[0][head[0]].d = -1;
key = hash(a[0][head[0]].data);
visited[0][key] = 1;
a[0][head[0]].key = key;
if (getTarget(&a[0][head[0]])) return head[0];
strcpy(a[1][head[1]].data, target);
for (int i = 0; i < N; i++) {
if (target[i] == N + '0') {
a[1][head[1]].blankPos = i;
break;
}
}
a[1][head[1]].parentIndex = NO_PARENT;
a[1][head[1]].d = -1;
key = hash(a[1][head[1]].data);
visited[1][key] = 1;
a[1][head[1]].key = key;
while (head[0] < tail[0] && head[1] < tail[1]) {
//从节点少的一边进行扩展
int currentQueue = (tail[0] - head[0] > tail[1] - head[1]) ? 1 : 0;
int blankPos = a[currentQueue][head[currentQueue]].blankPos;
for (int i = 0; i < 4; i++) {
if (canMoveStep[blankPos][i]) {
int newBlankPos = a[currentQueue][tail[currentQueue]].blankPos = blankPos + canMoveStep[blankPos][i];
strcpy(a[currentQueue][tail[currentQueue]].data, a[currentQueue][head[currentQueue]].data); //将移动后的新结点信息放入对列中。
char temp = a[currentQueue][tail[currentQueue]].data[blankPos]; //保存新结点的数字排列
a[currentQueue][tail[currentQueue]].data[blankPos] = a[currentQueue][tail[currentQueue]].data[newBlankPos];
a[currentQueue][tail[currentQueue]].data[newBlankPos] = temp;
a[currentQueue][tail[currentQueue]].parentIndex = head[currentQueue];
a[currentQueue][tail[currentQueue]].d = i;
key = hash(a[currentQueue][tail[currentQueue]].data);
a[currentQueue][tail[currentQueue]].key = key;
if (!visited[currentQueue][key]) {
visited[currentQueue][key] = 1;
//两个对列中有访问到相同的结点即表示找到了起点到终点的路径。
int otherQueue = currentQueue ? 0 : 1;
if (visited[otherQueue][key]) return tail[currentQueue] << 1 | currentQueue;
tail[currentQueue]++;
}
}
}
head[currentQueue]++; //处理下一个入列结点
}
return -1;
}
void initMoveFlags()
{
memset(canMoveStep, 0, sizeof(canMoveStep));
for (int i = 0; i < N; i++) {
canMoveStep[i][0] = i % C != 0 ? -1 : 0; //第0列不能向左搜索
canMoveStep[i][2] = (i + 1) % C != 0; //最后一列不能向右搜索
canMoveStep[i][1] = i < C ? 0 : -C; //第0行不能向上搜索
canMoveStep[i][3] = i >= (N - C) ? 0 : C; //最后一行不能向下搜索
}
}
void print(int result)
{
if (result == -1) {
printf("unsolvable\n");
return;
}
//在哪个对列中被找到及在当前对列中的下标。
int resultQueue = result & 1, resultIndex = result >> 1;
int key = hash(a[resultQueue][resultIndex].data);
char step[FACTORIAL_N]; //存放移动路径
int start = FACTORIAL_N / 2, end = start;
int firstQueueIndex = 1, sencondQueueIndex = 1;;
if (resultQueue == 1) {
while (a[0][firstQueueIndex].key != key) firstQueueIndex++;
sencondQueueIndex = resultIndex;
}
else {
firstQueueIndex = resultIndex;
while (a[1][sencondQueueIndex].key != key) sencondQueueIndex++;
}
int currentIndex = firstQueueIndex;
// //d表示从父节点的哪个方向来,如果当前节点没有父节点则表示是开始结点或结束结点。
while (a[0][currentIndex].parentIndex != NO_PARENT) {
step[--start] = direction[a[0][currentIndex].d];
currentIndex = a[0][currentIndex].parentIndex;
}
currentIndex = sencondQueueIndex;
while (a[1][currentIndex].parentIndex != NO_PARENT) {
//双向广搜相对搜索到相同结点时,从相同结点开始到结束结点是反向往回走,即与搜索到的方向相反。
step[end++] = direction[(a[1][currentIndex].d + 2) % 4];
currentIndex = a[1][currentIndex].parentIndex;
}
for (int i = start; i < end; i++) {
printf("%c", step[i]);
}
printf("\n");
}
int main()
{
//freopen("in.txt", "r", stdin);
node firstNode;
initMoveFlags();
char ch[2];
for (int i = 0; i < N; i++) {
scanf("%s", ch);
if (ch[0] == 'x') {
firstNode.data[i] = N + '0';
firstNode.blankPos = i;
}
else {
firstNode.data[i] = ch[0];
}
}
firstNode.data[N] = '\0';
int result = dbfs(&firstNode);
print(result);
return 0;
}