题目概述
给出一个3*3的矩阵,其中有一个格子是空格,其他都是数字,每次移动可以将格子附近的数字移到格子中,同时原先的数字变成格子。给定一个初始状态和最终状态,求一个方案。
解题报告
显然状态顶多只有9!=362880个,所以我们就会想到Bfs。但是如何判断一个状态是否出现过呢?用set肯定是可以的,但效率不是太高(常数也大),用hash也是可以的,然而完美的hash函数是存不下来的(当成9进制数)。这里就有一个比较好用的东西叫康托展开,其实也是hash,但是hash函数不但是完美的,而且空间开销也达到理论下限:9!=362880,非常适合储存全排列。
其实康托展开并不难理解,以274563018(下面称为当前排列)为例:
第一位为2,那么第一位为0,1的所有排列都比当前排列小,总计2*8!个。
第一位为2,第二位为7,那么第二位为0,1,3,4,5,6(没有2,第一位用过了)的所有排列都比当前排列小,总计6*7!个。
第一位为2,第二位为7,第三位为4,那么第三位为0,1,3的所有排列都比当前排列小,总计3*6!个。
……
所以,假设排列有n位,a[i]表示i+1~n中比i位上的数小的数的个数,那么比当前排列小的排列总个数就等于:
a[1]∗(n−1)!+a[2]∗(n−2)!+a[3]∗(n−3)!+……+a[n]∗0!
康托展开还有逆运算,就是已知有多少个排列比当前排列小,求当前排列。以208745为例,过程非常类似转进制:
208745/(8!)=5,说明有5个数比第一位小,即0,1,2,3,4,所以第一位为5。然后208745%(8!)=7145。
7145/(7!)=1,说明有1个数比第二位小,即0,所以第二位为1。然后7145%(7!)=2105。
2105/(6!)=2,说明有2个数比第三位小,即0,2(没有1,第二位用过了),所以第二位为3,然后2105%(6!)=665。
……
有了康托展开和逆运算,这道题就迎刃而解了……才怪!我写完了Bfs之后狂TLE不止,怎么改都超时。然后当我A了POJ之后我发现HDU多组数组我才疯狂TLE,于是想到了预处理。所以我从最终状态开始遍历,预处理了所有起始状态,然后从TLE变成了156ms……
示例程序
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int maxn=362880;
const int fac[9]={1,1,2,6,24,120,720,5040,40320};
const int fl[4]={-1,1,-3,3};
const int lst[9]={1,2,3,4,5,6,7,8,0};
const char op[4]={'l','r','u','d'};
struct data {int x,fa;char ch;};
int gl,who[maxn+5],fst[9];
data que[maxn+5];
bool vis[maxn+5];
void Travel(int now)
{
if (now==1) return;
putchar(que[now].ch);Travel(que[now].fa);
}
bool check(int x,char ch)
{
if (x<0||x>8) return false;
if (ch=='l'&&x%3==2) return false;
if (ch=='r'&&x%3==0) return false;
return true;
}
int Contor(const int *a) //康托展开
{
int pos=0;
for (int i=0;i<=8;i++)
{
int tot=0;
for (int j=i+1;j<=8;j++) tot+=a[i]>a[j]; //有tot个数比a[i]小
pos+=tot*fac[9-i-1];
}
return pos+1;
}
void INV_Contor(int pos,int *a) //康托展开逆运算
{
bool vis[9];pos--;memset(vis,0,sizeof(vis));
for (int i=0;i<=8;i++)
{
int now=pos/fac[9-i-1],j;
for (j=0;j<=8;j++) if (!vis[j])
if (now==0) break; else now--; //第now个出现的就是第i位
a[i]=j;vis[j]=true; //j出现过了,标记
pos%=fac[9-i-1];
}
}
int getp(int *a) {for (int i=0;i<=8;i++) if (!a[i]) return i;}
bool Bfs()
{
memset(vis,0,sizeof(vis));
int Head=0,Tail=0;que[++Tail]=(data){Contor(lst),0,0};
while (Head!=Tail)
{
int x[9],now[9],p;
INV_Contor(que[++Head].x,x);p=getp(x); //逆运算求当前状态x
for (int i=0;i<=3;i++)
if (check(p+fl[i],op[i]))
{
memcpy(now,x,sizeof(now));swap(now[p],now[p+fl[i]]);
int pos=Contor(now); //用康托展开将now变为数字
if (!vis[pos])
{
que[++Tail]=(data){pos,Head,op[i^1]}; //反向处理,所以是op[i^1]
vis[pos]=true;who[pos]=Tail;
}
}
}
return false;
}
char getrch()
{
char ch=getchar();
while (('9'<ch||ch<'0')&&ch!='x')
{
if (ch==EOF) return EOF;
ch=getchar();
}
if (ch=='x') return '0';
return ch;
}
int main()
{
freopen("program.in","r",stdin);
freopen("program.out","w",stdout);
Bfs();char ch;
while ((ch=getrch())!=EOF)
{
fst[0]=ch-48;for (int i=1;i<=8;i++) fst[i]=getrch()-48;
int pos=Contor(fst);
if (!who[pos]) printf("unsolvable"); else Travel(who[pos]);
putchar('\n');
}
return 0;
}