POJ 1077 Eight(八数码) 双向广搜

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;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值