POJ 3567 Eight II (八数码问题+bfs+康托展开)
题目链接
http://acm.hdu.edu.cn/showproblem.php?pid=3567
解题思路
这道题是POJ 1077 Eight的升级版, 区别在于POJ1077的终点是确定的,那么其他情况都是可以由这一种情况推出. 这道题的起点和终点似乎都是不确定的,如果暴力搜索的话一定会超时,这就很难办. 可以从另一个角度来思考, 我们根据起点中'X'的位置来进行分类,而其他位置的棋子用其编号(1~8)来表示即可,这样把起点从\(9!\)中情况缩小到了9种情况,以这9种情况事先进行一个搜索就可以了.
输出路径要求是字典最小序,以d, l, r, u的顺序搜索即可
搜索思路
这道题的另一个难点----也是八数码问题的核心----在于如何标识每一种情况, 方法是利用康托展开算法进行哈希
康托展开是一个全排列到一个自然数的双射,常用于构建哈希表时的空间压缩。 康托展开的实质是计算当前排列在所有由小到大全排列中的顺序,因此是可逆的。
康托展开的转换公式: \(X=a_n(n-1)!+a_{n-1}(n-2)!+\cdots+a_1\cdot0!\),其中\(a_i\)为整数,并且\(0\leq a_i<i,1\leq i\leq n\)
对公式的解释: 设\(m_i\)是数组中从后往前数第i个数, 则\(a_i\)是\(m_i\)后面的数中比\(m_i\)小的数的个数
举例:3 5 7 4 1 2 9 6 8 展开为 98884。因为X=2*8!+3*7!+4*6!+2*5!+0*4!+0*3!+2*2!+0*1!+0*0!=98884.
解释:
排列的第一位是3,比3小的数有两个,以这样的数开始的排列有8!个,因此第一项为2*8!
排列的第二位是5,比5小的数有1、2、3、4,由于3已经出现,因此共有3个比5小的数,这样的排列有7!个,因此第二项为3*7!
以此类推,直至0*0!
----维基百科
AC代码
#include <algorithm>
#include <iostream>
#include <cstring>
#include <string>
#include <queue>
#include <stack>
using namespace std;
const int maxn = 4e5;
int a[10];
int cnt[10];
int tmp[9];
stack<char> ans;
char sp[15], ep[15];
int pos, tar[9];
int mp[9][9] = {//9代表'X'
9, 1, 2, 3, 4, 5, 6, 7, 8,
1, 9, 2, 3, 4, 5, 6, 7, 8,
1, 2, 9, 3, 4, 5, 6, 7, 8,
1, 2, 3, 9, 4, 5, 6, 7, 8,
1, 2, 3, 4, 9, 5, 6, 7, 8,
1, 2, 3, 4, 5, 9, 6, 7, 8,
1, 2, 3, 4, 5, 6, 9, 7, 8,
1, 2, 3, 4, 5, 6, 7, 9, 8,
1, 2, 3, 4, 5, 6, 7, 8, 9
};
struct node{
int num[9];
int pos;
int pre;
char path;
node(): pos(-1), path(0){}
}que[9][maxn];
int cant(int *ori){ //康托展开
for (int i = 1; i < 10; ++i){
cnt[i] = i - 1;
}
int s = 0;
for (int i = 0; i < 9; ++i){
s += cnt[ori[i]] * a[8 - i];
for (int j = ori[i]; j < 10; ++j){
--cnt[j];
}
}
return s;
}
void bfs(const int p, int rt){
queue<int> q;
q.push(rt);
int ct, frt;
node * cur;
while (!q.empty()){
frt = q.front();
q.pop();
cur = &que[p][frt];
memcpy(tmp, cur->num, sizeof tmp);
//0: down
if (cur->pos < 6){
swap(tmp[cur->pos], tmp[cur->pos + 3]);
ct = cant(tmp);
if (que[p][ct].pos == -1){
que[p][ct].pos = cur->pos + 3;
memcpy(que[p][ct].num, tmp, sizeof tmp);
que[p][ct].pre = frt;
que[p][ct].path = 'd';
q.push(ct);
}
swap(tmp[cur->pos], tmp[cur->pos + 3]);
}
//1: left
if (cur->pos % 3 != 0){
swap(tmp[cur->pos], tmp[cur->pos - 1]);
ct = cant(tmp);
if (que[p][ct].pos == -1){
que[p][ct].pos = cur->pos - 1;
memcpy(que[p][ct].num, tmp, sizeof tmp);
que[p][ct].pre = frt;
que[p][ct].path = 'l';
q.push(ct);
}
swap(tmp[cur->pos], tmp[cur->pos - 1]);
}
//2: right
if (cur->pos % 3 != 2){
swap(tmp[cur->pos], tmp[cur->pos + 1]);
ct = cant(tmp);
if (que[p][ct].pos == -1){
que[p][ct].pos = cur->pos + 1;
memcpy(que[p][ct].num, tmp, sizeof tmp);
que[p][ct].pre = frt;
que[p][ct].path = 'r';
q.push(ct);
}
swap(tmp[cur->pos], tmp[cur->pos + 1]);
}
//3: up
if (cur->pos >= 3){
swap(tmp[cur->pos], tmp[cur->pos - 3]);
ct = cant(tmp);
if (que[p][ct].pos == -1){
que[p][ct].pos = cur->pos - 3;
memcpy(que[p][ct].num, tmp, sizeof tmp);
que[p][ct].pre = frt;
que[p][ct].path = 'u';
q.push(ct);
}
swap(tmp[cur->pos], tmp[cur->pos - 3]);
}
}
}
int main(){
a[0] = 1;
for (int i = 1; i < 10; ++i){
a[i] = i * a[i - 1];
}
for (int i = 0; i < 9; ++i){
int ct = cant(mp[i]);
que[i][ct].pos = i;
memcpy(que[i][ct].num, mp[i], sizeof que[i][ct].num);
bfs(i, ct);
}
int cs;
scanf("%d", &cs);
for (int t = 1; t <= cs; ++t){
scanf("%s", sp);
scanf("%s", ep);
int p;//'X'的位置
for (p = 0; ; ++p){
if (sp[p] == 'X'){
break;
}
}
int tp;
for (tp = 0; ; ++tp){
if (ep[tp] == 'X'){
break;
}
}
//用棋子的编号来标识终点
for (int i = 0; i < 9; ++i){
for (int j = 0; j < 9; ++j){
if (ep[i] == sp[j]){
tar[i] = mp[p][j];
}
}
}
int ct = cant(tar);
while (!ans.empty()) ans.pop();
while (que[p][ct].path){
ans.push(que[p][ct].path);
ct = que[p][ct].pre;
}
printf("Case %d: %d\n", t, ans.size());
while (!ans.empty()){
printf("%c", ans.top());
ans.pop();
}
printf("\n");
}
return 0;
}