hdu 1043/poj 1077 Eight (八数码 经典搜索题 bfs + 康托展开)

时间:2022-12-09 09:48:08

Eight

Time Limit: 10000/5000 MS (Java/Others) Memory Limit: 65536/32768 K (Java/Others)
Special Judge

Problem 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:

hdu 1043/poj 1077 Eight (八数码 经典搜索题 bfs + 康托展开)

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:

hdu 1043/poj 1077 Eight (八数码 经典搜索题 bfs + 康托展开)

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 Input
2 3 4 1 5 x 7 6 8

Sample Output
ullddrurdllurdruldr

十分经典的一道题,题意就不解释了。解法很多,知名度高到产生了“不做此题人生不完整”的说法。

现在看这道题和普通的bfs似乎差别不大,核心难点是记录状态,九位数太大了,开个数组内存会炸,同时还有大量的空间没有利用到。不过只要学习过hash,很容易就会想到利用hash算法将所有的状态映射到一个较小的区间上。

于是大体的思路就是:搜索+hash记录状态。

但只用bfs会出现超时的现象,因此也有人使用a*启发式搜索。

现在流传的主要解法有:
1.bfs单广(这个方法貌似只能过poj的单组数据,hdu是多组数据,因此会T);
2.bfs倒搜打表(博主的第一版代码可以过hdu,但poj上会T,第二版代码也可以过poj。个人认为此法是针对hdu 1043的最优解法,poj则正搜更佳);
3.a*(网上流传各种估价函数,一直无法正视玄幻的估价函数因此现对此解法不做任何探讨)。
4.bfs双广(似乎是写得好就两边都能过。然而不会……也许什么时候学了双广会回来更新题解吧)。

目前此题做题情况:
1.单广+hash过poj,hdu会T,已证实此版代码是错误的(没贴)。第三版正确代码单广正向+cantor过poj,hdu会T;
2.单广倒搜打表+cantor过hdu,第一版代码在poj会T,修改后的第二版代码在poj也能ac,并且在hdu上空间复杂度大大降低,时间也得到优化。

交过poj的单广+BKDRHash先不说效率,这个的解法本身就是错误的,因为BKDRHash表所能记录的状态总共为249997种,而八数码所有的状态数加起来是9!即362880,早就超过了它能记录的范围,这样一来就有状态记录不全的风险存在,会出错是必然,能过纯粹是运气好,仗着人家poj只有一组数据。hdu能过但是poj会T的,一般是优化不好或者是STL消耗时间太长,所以代码最好能同时交得过两个oj。

重新学习了一下康托展开,然后用它代替了BKDRHash的计算方式,记录状态所用的思想本质上还是hash的思想。

康托展开公式:
X = a[n] * (n - 1)! + a[n - 1] * (n - 2)! + ... + a[i] * (i - 1)! + ... + a[2] * 1! + a[1] * 0!;

至于a[i]是个啥,我觉得百科说的那一句总结看不懂,还是举例子来的直白。

就比如八数码:1, 2, 3, 4, 5, 6, 7, 8, 9(为了方便把x替换成9)
我们将样例中的s = {‘2’, ‘3’, ‘4’, ‘1’, ‘5’, ‘x’, ‘7’, ‘6’, ‘8’}替换成s = {‘2’, ‘3’, ‘4’, ‘1’, ‘5’, ‘9’, ‘7’, ‘6’, ‘8’}

这个样例替换后就变成了八数码的一个排列,然后来分析:
X(s) = a[9] * 8! + a[8] * 7! + a[7] * 6! + a[6] * 5! + a[5] * 4! + a[4] * 3! + a[3] * 2! + a[2] * 1! + a[1] * 0!;

a[9] = ‘2’这个元素在子数组{'2', '3', '4', '1', '5', '9', '7', '6', '8'}中是排位第几大的元素(从0开始算)→ 1
a[8] = ‘3’这个元素在子数组{'3', '4', '1', '5', '9', '7', '6', '8'}中是排位第几大的元素 → 1
a[7] = ‘4’这个元素在子数组{'4', '1', '5', '9', '7', '6', '8'}中是排位第几大的元素 → 1
a[6] = ‘1’这个元素在子数组{'1', '5', '9', '7', '6', '8'}中是排位第几大的元素 → 0
a[5] = ‘5’这个元素在子数组{'5', '9', '7', '6', '8'}中是排位第几大的元素 → 0
a[4] = ‘9’这个元素在子数组{'9', '7', '6', '8'}中是排位第几大的元素 → 3
a[3] = ‘7’这个元素在子数组{'7', '6', '8'}中是排位第几大的元素 → 1
a[2] = ‘6’这个元素在子数组{'6', '8'}中是排位第几大的元素 → 0
a[1] = ‘8’这个元素在子数组{'8'}中是排位第几大的元素 → 0

于是X(s) = 8! + 7! + 6! + 3 * 3! + 2! = 46100

计算上限为9! + 8! + 7! + 6! + 5! + 4! + 3! + 2! + 1 = 409113,将数组大小定为410000,这样一来存储区间就在可接受范围内了。

第二版代码已经得到了时间和空间优化,各函数及变量含义请参考第一版代码,存图回溯请参考第二版代码。 poj上用bfs+BKDRHash虽然可过,但是我很清楚代码是错的,因此不贴出来。更新后第三版代码是通过poj的正向bfs+康托展开,三版代码是一版一版改下来的,请按顺序参考。

PS: 第一版的代码在hdu上跑完打表和多组数据用了156ms,然而同样的代码在poj上却会超时(时限1000ms),似乎只能用正向搜索来写。之前朋友写的bfs在poj上交c++超时,用g++交就只跑时限一半的时间。不是很懂。


10.6晚上10点半更新,经过修改的第二版代码将打表部分从暴力存vector改成了指针+存图,少了STL(其实我觉得卡了时限的主要还是“str[temp] = str[tmp.itshash];”这一句),多了一个简单的答案搜索过程(实际上与循环同等时间复杂度),所需时间减少,并且空间复杂度得到了较好的优化。poj已通过。


10.7中午1点更新,第三版在第二版代码的基础上改成正搜+cantor,poj通过,并且速度比打表版本要快一些,但内存并没有得到优化,不知和代码姿势是否有关……hdu仍然是T,bfs正搜+cantor这条路大概在hdu上是走不通了,过一段时间如果学a*可能会补一个a *的版本。

第一版代码单广倒搜打表+cantor(只通过了hdu,poj超时):

#include <iostream>
#include <queue>
#include <string>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define M 410000

using namespace std;

struct node
{
char arr[9];//记录状态用的数组
int id, itshash;//id记录x的下标,itshash记录此状态的hash值
}eight, tmp, t, ed;

int to[4] = {-1, -3, 1, 3};
vector<char> str[M];
char To[4] = {'r', 'd', 'l', 'u'};//对应to数组,注意因为是倒打表,所以上和下,左和右是反的。
int f[9] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320};//康托展开辅助数组。
bool Hash[M];//判重用的vis

int cantor(char *s)//康托展开,理解原理之后即可手写
{
int ans = 0;
for(int i = 0; i < 9; i++)
{
int cnt = 0;
for(int j = i + 1; j < 9; j++)
{
if(s[i] > s[j])
cnt++;
}
ans += cnt * f[9 - i - 1];
}
return ans;
}

int hashit(char *s)//相当于判重
{
int id = cantor(s);
if(Hash[id])
return 0;
else
{
Hash[id] = true;
return id;
}
}

void bfs(node &s)//倒搜遍历打表
{
queue<node> Q;
s.itshash = hashit(s.arr);
Q.push(s);

while(!Q.empty())
{
tmp = Q.front();
Q.pop();
for(int i = 0; i < 4; i++)
{
if((i == 0 && !(tmp.id % 3)) || (i == 1 && tmp.id < 3) || (i == 2 && !((tmp.id - 2) % 3)) || ((i == 3) && tmp.id > 5))//边界判断
continue;
t = tmp;
t.arr[tmp.id + to[i]] = t.arr[tmp.id];
t.arr[tmp.id] = tmp.arr[tmp.id + to[i]];
t.id = tmp.id + to[i];
int temp = hashit(t.arr);
if(temp)
{
t.itshash = temp;
str[temp] = str[tmp.itshash];
str[temp].push_back(To[i]);
Q.push(t);
}
}
}
return;
}

int main()
{
memset(Hash, false, sizeof(Hash));//全局变量初始值为0,这个其实不写也没啥影响。
ed.arr[0] = '1', ed.arr[1] = '2', ed.arr[2] = '3', ed.arr[3] = '4', ed.arr[4] = '5', ed.arr[5] = '6', ed.arr[6] = '7', ed.arr[7] = '8', ed.arr[8] = '9';
ed.id = 8;
bfs(ed);
while(~scanf("%s %s %s %s %s %s %s %s %s", &eight.arr[0], &eight.arr[1], &eight.arr[2], &eight.arr[3], &eight.arr[4], &eight.arr[5], &eight.arr[6], &eight.arr[7], &eight.arr[8]))
{
for(int i = 0; i < 9; i++)
{
if(eight.arr[i] == 'x')
eight.arr[i] = '9';
}
int temp = cantor(eight.arr);
if(temp == ed.itshash)//注意特殊情况的处理
{
printf("lr\n");
}
else if(str[temp].size())
{
for(int i = str[temp].size() - 1; i >= 0; i--)
printf("%c", str[temp][i]);
printf("\n");
}
else
printf("unsolvable\n");
}
return 0;
}

运行结果:
hdu 1043/poj 1077 Eight (八数码 经典搜索题 bfs + 康托展开)


第二版代码单广倒搜打表+cantor(经过优化,hdu花费时间与内存均减少,poj通过):

#include <iostream>
#include <queue>
#include <string>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define M 410000

using namespace std;

struct node
{
char arr[9];
int id, itshash;
}eight, tmp, t, ed;

struct step//新增存储用的图
{
int nxt;
char go;
}g[M];

int to[4] = {-1, -3, 1, 3};
char To[4] = {'r', 'd', 'l', 'u'};
int f[9] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320};
bool Hash[M];

int cantor(char *s)
{
int ans = 0;
for(int i = 0; i < 9; i++)
{
int cnt = 0;
for(int j = i + 1; j < 9; j++)
{
if(s[i] > s[j])
cnt++;
}
ans += cnt * f[9 - i - 1];
}
return ans;
}

int hashit(char *s)
{
int id = cantor(s);
if(Hash[id])
return -1;
else
{
Hash[id] = true;
return id;
}
}

void bfs(node &s)
{
queue<node> Q;
s.itshash = hashit(s.arr);
Q.push(s);

while(!Q.empty())
{
tmp = Q.front();
Q.pop();
for(int i = 0; i < 4; i++)
{
if((i == 0 && !(tmp.id % 3)) || (i == 1 && tmp.id < 3) || (i == 2 && !((tmp.id - 2) % 3)) || ((i == 3) && tmp.id > 5))
continue;
t = tmp;
t.arr[tmp.id + to[i]] = t.arr[tmp.id];
t.arr[tmp.id] = tmp.arr[tmp.id + to[i]];
t.id = tmp.id + to[i];
int temp = hashit(t.arr);
if(temp != -1)
{
t.itshash = temp;
g[temp].nxt = tmp.itshash;//改动了两行代码
g[temp].go = To[i];
Q.push(t);
}
}
}
return;
}

void dfs(int cur)//简单的递归,用循环写也完全可行。
{
printf("%c", g[cur].go);
if(g[cur].nxt)
dfs(g[cur].nxt);
return;
}

int main()
{
ed.arr[0] = '1', ed.arr[1] = '2', ed.arr[2] = '3', ed.arr[3] = '4', ed.arr[4] = '5', ed.arr[5] = '6', ed.arr[6] = '7', ed.arr[7] = '8', ed.arr[8] = '9';
ed.id = 8;
bfs(ed);
while(~scanf("%s %s %s %s %s %s %s %s %s", &eight.arr[0], &eight.arr[1], &eight.arr[2], &eight.arr[3], &eight.arr[4], &eight.arr[5], &eight.arr[6], &eight.arr[7], &eight.arr[8]))
{
for(int i = 0; i < 9; i++)
{
if(eight.arr[i] == 'x')
eight.arr[i] = '9';
}
int temp = cantor(eight.arr);
if(!temp)
{
printf("lr\n");
}
else//注意这里的改动
{
if(hashit(eight.arr) != -1)
printf("unsolvable\n");
else
{
dfs(temp);
printf("\n");
}
}
}
return 0;
}

运行结果:
poj:hdu 1043/poj 1077 Eight (八数码 经典搜索题 bfs + 康托展开)
hdu:hdu 1043/poj 1077 Eight (八数码 经典搜索题 bfs + 康托展开)
poj的机子是真的比hdu慢……


第三版代码单广正搜+cantor(poj上比倒搜打表快一些,hdu上T):

#include <iostream>
#include <queue>
#include <string>
#include <cstdio>
#include <cstring>
#include <algorithm>
#define M 410000

using namespace std;

struct node
{
char arr[9];
int id, itshash;
}eight, tmp, t;

struct step
{
int last;
char go;
}g[M];

int to[4] = {-1, -3, 1, 3};
char To[4] = {'l', 'u', 'r', 'd'};//这里改回来了
int f[9] = {1, 1, 2, 6, 24, 120, 720, 5040, 40320};
int Hash[M];//改变数据类型

int cantor(char *s)
{
int ans = 0;
for(int i = 0; i < 9; i++)
{
int cnt = 0;
for(int j = i + 1; j < 9; j++)
{
if(s[i] > s[j])
cnt++;
}
ans += cnt * f[9 - i - 1];
}
return ans;
}

int hashit(char *s)
{
int id = cantor(s);
if(Hash[id] != -1)//这里做了修改
return -1;
else
{
Hash[id] = 1;
return id;
}
}

bool bfs(node &s)//返回类型和少许代码改变
{
queue<node> Q;
s.itshash = hashit(s.arr);
Q.push(s);

while(!Q.empty())
{
tmp = Q.front();
Q.pop();
for(int i = 0; i < 4; i++)
{
if((i == 0 && !(tmp.id % 3)) || (i == 1 && tmp.id < 3) || (i == 2 && !((tmp.id - 2) % 3)) || ((i == 3) && tmp.id > 5))
continue;
t = tmp;
t.arr[tmp.id + to[i]] = t.arr[tmp.id];
t.arr[tmp.id] = tmp.arr[tmp.id + to[i]];
t.id = tmp.id + to[i];
int temp = hashit(t.arr);

if(temp != -1)
{
t.itshash = temp;
g[temp].last = tmp.itshash;
g[temp].go = To[i];
if(!temp)//这里做了修改
{
return true;
}
Q.push(t);
}
}
}
return false;
}

void dfs(int cur)//代码顺序有改动
{
if(g[cur].last)
{
dfs(g[cur].last);
printf("%c", g[cur].go);
}
return;
}

int main()
{
while(~scanf("%s %s %s %s %s %s %s %s %s", &eight.arr[0], &eight.arr[1], &eight.arr[2], &eight.arr[3], &eight.arr[4], &eight.arr[5], &eight.arr[6], &eight.arr[7], &eight.arr[8]))
{
memset(Hash, -1, sizeof(Hash));//原排列1 2 3 4 5 6 7 8 9的hash值是0,所以把hash初始化成-1。
//这里考虑多组输入的话还应该将存储用图g也初始化,由于poj只有一组数据就省去了。
for(int i = 0; i < 9; i++)
{
if(eight.arr[i] == 'x')
{
eight.arr[i] = '9';
eight.id = i;
}
}

int temp = cantor(eight.arr);
if(!temp)
{
printf("lr\n");
}
else
{
if(!bfs(eight))//这一段有改动
{
printf("unsolvable\n");
}
else
{
dfs(0);
printf("\n");
}
}
}
return 0;
}

运行结果:
hdu 1043/poj 1077 Eight (八数码 经典搜索题 bfs + 康托展开)