Time Limit: 1000MS | Memory Limit: 65536K | |||
Total Submissions: 32045 | Accepted: 13910 | Special Judge |
Description
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
1 2 3 x 4 6 7 5 8
is described by this list:
1 2 3 x 4 6 7 5 8
Output
Sample Input
2 3 4 1 5 x 7 6 8
Sample Output
ullddrurdllurdruldr
题意:八数码问题,输入形如 2 3 4 1 5 x 7 6 8 的一行值,代表
2 3 4
1 5 x
7 6 8
最终要到达的状态为
1 2 3
4 5 6
7 8 x
也就是 1 2 3 4 5 6 7 8 x
只能移动x与其相邻的格子交换,求用最少步数到达目标状态的每一步操作,对x的上、下、左、右移动分别输出'u'、'd'、'l'、'r'(单个字符),中间无空格
解题思路:
首先想到的是暴力广搜,用set或者康拓展开求hash值判重,记录每一步,估计会超时。
后来想到状态数一共 9! = 362880 个,打表很不错,后来发现确实是这样,特别是HDU上的是多组测试数据,打表后速度很快,而在POJ上速度则比较慢。
如果想优化在POJ上的速度,双向BFS或者A*都是不错的选择。值得一提的是,这两种方法在HDU上都没有 普通BFS + hash + 打表 快,而且这两种方法无法打表,因为他们都对BFS进行了优化,优化避免了遍历大部分无用的状态,而打表需要记录所有状态。
双向BFS指用两个队列,两种标记进行从初始状态和目标状态两个点同时进行遍历,当他们汇聚于一点时,初始点到汇聚点+汇聚点到目标点的路径即为最短路径,因为两边是同时扩展的,深度相同,当第一次相遇的时候肯定为最优解。
最后是A*算法
A*算法就是利用启发式函数,计算初始点到达某个点和估计这个点到达目标点的代价,启发式函数得到的值越小,说明这个点越靠近初始点到目标点的最短路径上
即: F = G + H
G 表示从起点移动到当前点的代价(包括但不限于步数)
H 表示从当前点移动到终点的代价(常见的有曼哈顿距离、对角线距离以及欧几里得距离)
A*算法步骤:1. 从起点A开始, 把它作为待处理的方格存入一个"开启列表", 开启列表就是一个等待检查方格的列表.
2. 寻找起点A周围可以到达的方格, 将它们放入"开启列表", 并设置它们的"父方格"为A.
3. 从"开启列表"中删除起点 A, 并将起点 A 加入"关闭列表", "关闭列表"中存放的都是不需要再次检查的方格.
4. 从 "开启列表" 中选择 F 值最低的方格 C,把它从 "开启列表" 中删除, 并放到 "关闭列表" 中.
5. 检查它所有相邻并且可以到达 (障碍物和 "关闭列表" 的方格都不考虑) 的方格. 如果这些方格还不在 "开启列表" 里的话, 将它们加入 "开启列表", 计算这些方格的 G, H 和 F 值各是多少, 并设置它们的 "父方格" 为 C.
6. 如果某个相邻方格 D 已经在 "开启列表" 里了, 检查如果用新的路径 (就是经过C 的路径) 到达它的话, G值是否会更低一些, 如果新的G值更低, 那就把它的 "父方格" 改为目前选中的方格 C, 然后重新计算它的 F 值和 G 值 (H 值不需要重新计算, 因为对于每个方块, H 值是不变的). 如果新的 G 值比较高, 就说明经过 C 再到达 D 不是一个明智的选择, 因为它需要更远的路, 这时我们什么也不做.
这里有一篇很好的A*算法讲解文章,可以参考一下:http://blog.csdn.net/b2b160/article/details/4057781
代码:
/*
POJ 1077 AC G++ 79MS 1912K
HDU 1043 AC G++ 1653MS 3008K
*/
#include <stdio.h>
#include <string.h>
#include <queue>
#include <map>
#define target 123456780
using namespace std;
char dirs[4] = {'u', 'd', 'l', 'r'}; //对方向的操作
int changePosition[9][4] = { //changePosition[i][j]表示x在位置i做了dirs[j]操作之后到达的位置
{-1, 3, -1, 1},
{-1, 4, 0, 2},
{-1, 5, 1, -1},
{ 0, 6, -1, 4},
{ 1, 7, 3, 5},
{ 2, 8, 4, -1},
{ 3, -1, -1, 7},
{ 4, -1, 6, 8},
{ 5, -1, 7, -1}
};
struct mnode { //map的结点
int value; //该状态的int值
int g; //初始点到当前点的代价
int pre; //前一个状态的hash值(在map中的key),用来反向输出
int direction; //前一个状态移动到当前状态的方向
};
struct qnode { //优先队列的结点
int f; //估价函数f = g + h,g为初始点到当前点的代价,h为启发函数,是当前点到目标点的估计代价
int key; //这个状态对应map中的key,即状态的hash值
bool operator < (const qnode &a) const {
return f > a.f; //按f最小值优先
}
};
map<int, mnode> mmap; //key为状态代表的hash值,value为mnode结点,代表一个状态
priority_queue<qnode> que; //优先队列,每次取估价函数最小的状态判断
//初始化
void init()
{
mmap.clear();
while(!que.empty())
{
que.pop();
}
}
//将输入的字符串转化为int类型值(去除中间空格)
int StringToInt(char *s)
{
int ans = 0;
int len = strlen(s);
for (int i = 0; i < len; i++)
{
if (s[i] >= '0' && s[i] <= '9')
{
ans = ans * 10 + s[i] - '0';
}
else if (s[i] == 'x')
{
ans *= 10;
}
}
return ans;
}
//将int类型的状态值value转化为一维数组储存在全局的v数组里面,并返回0在value中的位置(从0开始)
int IntToArray(int value, int* v)
{
int location = 0;
for (int i = 8; i >= 0; i--)
{
if (value % 10 == 0)
{
location = i;
}
v[i] = value % 10;
value /= 10;
}
return location;
}
/*
判断无解的情况:一个状态表示成一维的形式,求出除0之外所有数字的逆序数之和,
若两个状态的逆序奇偶性相同,则可相互到达,否则不可相互到达。
求逆序数的方法参考http://blog.csdn.net/sdfgdbvc/article/details/51123326
此题因为只有九个数字,所以进行了循环比较求逆序数
*/
bool Reachable(int value)
{
int v[9];
int location = IntToArray(value, v);
int sum = 0;
for (int i = 0; i <= 8; i++)
{
if (i == location) continue;
for (int j = 0; j < i; j++)
{
if (j == location) continue;
if (v[j] > v[i])
{
sum++;
}
}
}
//目标状态为1234567890,除0外逆序数为0,所以初始状态的逆序数要为偶数才能到达
if (sum % 2 == 1)
{
return false;
}
return true;
}
int fac[] = {40320, 5040, 720, 120, 24, 6, 2, 1, 1}; //用来以康拓展开的方法求hash值,分别为8到0的接触
/*
康拓展开求hash值
康拓展开指一个数在它各个位数字的排列中排第几大
康托展开的公式是 X=an*(n-1)!+an-1*(n-2)!+...+ai*(i-1)!+...+a2*1!+a1*0!
其中,ai为当前未出现的元素中是排在第几个(从0开始)。
康拓展开求hash值可以保证每一个状态求得的hash值都不相同,不必再散列
ps:总共状态数为9! = 362 880
如果用朴素的除留取余法+线性探测再散列对每一个状态的value值求hash值,速度会更快一些
*/
int Cantor(int value)
{
int v[9];
int sum = 0;
IntToArray(value, v);
for (int i = 0; i <= 8; i++)
{
int num = 0;
for (int j = i + 1; j <= 8; j ++)
{
if (v[j] < v[i])
{
num++;
}
}
sum += num * fac[i];
}
return sum + 1;
}
int steps[9][9] = { //steps[i][j] 表示 位置i移动到位置j需要几步,用来求启发函数h
{0, 1, 2, 1, 2, 3, 2, 3, 4},
{1, 0, 1, 2, 1, 2, 3, 2, 3},
{2, 1, 0, 3, 2, 1, 4, 3, 2},
{1, 2, 3, 0, 1, 2, 1, 2, 3},
{2, 1, 2, 1, 0, 1, 2, 1, 2},
{3, 2, 1, 2, 1, 0, 3, 2, 1},
{2, 3, 4, 1, 2, 3, 0, 1, 2},
{3, 2, 3, 2, 1, 2, 1, 0, 1},
{4, 3, 2, 3, 2, 1, 2, 1, 0}
};
//得到启发函数值f,为当前状态中每个数字直接到达目标状态中该数字所处位置的步数之和
int getEvaluation(int value)
{
int ans = 0;
for (int i = 8; i >= 0; i--)
{
int num = value % 10 - 1;
if (num == -1)
{
ans += steps[i][8];
}
else
{
ans += steps[i][num];
}
value /= 10;
}
return ans;
}
/*
输出答案
ps:此题为Special Judge,可以接受多种答案
比如样例输入2 3 4 1 5 x 7 6 8
我的程序会输出 dlurullddrurdllurdr
而题目中样例输出ullddrurdllurdruldr
只输出任意一种即可
*/
void print_answer(int key)
{
int pre = mmap[key].pre;
if (pre != -1)
{
print_answer(pre);
printf("%c", mmap[key].direction);
}
}
//A*算法主体
void Astar(int value)
{
int hashValue = Cantor(value);
mmap[hashValue] = ((mnode) {value, 0, -1, 's'});
que.push(((qnode) {getEvaluation(value), hashValue})); //初始状态
while(!que.empty())
{
qnode current = que.top(); que.pop();
int t = mmap[current.key].value;
if (t == target)
{
print_answer(current.key);
printf("\n");
return;
}
int v[9];
int loc = IntToArray(t, v);
for (int i = 0; i < 4; i++)
{
if (changePosition[loc][i] == -1)
{
continue;
}
t = 0;
for (int j = 0; j <= 8; j++) //求新的交换过的状态值
{
if (j == loc)
{
t = t * 10 + v[changePosition[loc][i]];
}
else if (j == changePosition[loc][i])
{
t = t * 10 + v[loc];
}
else
{
t = t * 10 + v[j];
}
}
hashValue = Cantor(t);
if (mmap.find(hashValue) == mmap.end()) //该状态没有在mmap中记录,可以加入
{
int g = mmap[current.key].g + 1;
mmap[hashValue] = ((mnode) {t, g, current.key, dirs[i]});
que.push(((qnode) {g + getEvaluation(t), hashValue}));
}
}
}
}
int main()
{
char ch[50];
while(gets(ch) != NULL)
{
int value = StringToInt(ch);
if (!Reachable(value))
{
printf("unsolvable\n");
continue;
}
init();
Astar(value);
}
return 0;
}