八数码问题也称为九宫问题。在3×3的棋盘上摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。给出一个初始状态和一个目标状态,求出从初始状态转变成目标状态的移动棋子步数的最少值。
一般的目标状态是指下面这样的排列方式。
我们先讲解下面这几个子问题,从而一步一步解决它。
1.康托展开和逆康托展开
假设有A,B,C,D四个字母的一个排列D,B,A,C,现在要求这个排列是字典序中的第几个。康托展开的公式是X=an*(n-1)!+an-1*(n-2)!+...+ai*(i-1)!+...+a2*1!+a1*0!, 其中,ai为当前未出现的元素中是排在第几个(从0开始)。在这个例子中,X=a4*3!+a3*2!+a2*1!+a1*0!。
a4=D这个元素在子数组D,B,A,C中是第几大的元素。A是第0大的元素,B是第1大的元素,C是第2大的元素,D是第3大的元素,所以a4=3。
a3=B这个元素在子数组B,A,C中是第几大的元素。A是第0大的元素,B是第1大的元素,C是第2大的元素,所以a3=1。
a2=A这个元素在子数组A,C中是第几大的元素。A是第0大的元素,C是第1大的元素,所以a2=0。
a1=C这个元素在子数组C中是第几大的元素。因为子数组只有1个元素,所以a1=0。
所以,X=3*3!+1*2!+0*1!+0*0!=20。
如果已知某个排列是字典序中的第20个,求出这个排列的过程就是逆康托展开。
通过辗转相除法可以知道a4、a3、a2、a1的值,第一个元素是A,B,C,D中第3大的元素D,第二个元素是A,B,C中第1大的元素B,第三个元素是A,C中第0大的元素A,第四个元素是子数组C中第0大的元素C,所以排列为D,B,A,C。
2.怎样判断八数码问题是否有解
在分析之前,先引进逆序和逆序数的概念:对于棋子数列中任何一个棋子c[i](1≤i≤8),如果有j>i且c[j]c[i+1],那么交换之后c[i]的逆序数减1,而c[i+1]的逆序数不变。显然,空格与相邻棋子的交换不会改变棋局中棋子数列的逆序数的奇偶性。由于最终的逆序数是0,所以当初始状态棋局的棋子数列的逆序数是奇数时无解,偶数时有解。
3.启发式搜索(A*算法)
很多时候,BFS运行得较快,但是它找到的路径明显不是一条好的路径。
问题在于BFS是基于贪心策略的,它试图向目标移动尽管这不是正确的路径。由于它仅仅考虑到达目标的代价,而忽略了当前已花费的代价,于是尽管路径变得很长,它仍然继续走下去。1968年发明的A*算法就是把启发式方法(heuristic approaches)如BFS,和常规方法如Dijsktra算法结合在一起的算法。有点不同的是,类似BFS的启发式方法经常给出一个近似解而不是保证最佳解。然而,尽管A*基于无法保证最佳解的启发式方法,A*却能保证找到一条最短路径。在启发式搜索中,我们每次找到当前“最有希望是最短路径”的状态进行扩展。对于每个状态的我们用函数F来估计它是否有希望。
F=G+H
G:就是普通宽度优先搜索中的从起始状态到当前状态的代价,比如在这次的问题中,G就等于从起始状态到当前状态的最少步数。
H:是一个估计的值,表示从当前状态到目标状态估计的代价。
H是由我们自己设计的,H函数设计的好坏决定了A*算法的效率。H值越大,算法运行越快。但是在设计评估函数时,需要注意一个很重要的性质:评估函数的值一定要小于等于实际当前状态到目标状态的代价。否则虽然程序运行速度加快,但是可能在搜索过程中漏掉了最优解。相对的,只要评估函数的值小于等于实际当前状态到目标状态的代价,就一定能找到最优解。所以,在这个问题中我们可以将评估函数设定为1-8八数字当前位置到目标位置的曼哈顿距离之和。
成功的秘决在于,它把Dijkstra算法(靠近初始点的结点)和BFS算法(靠近目标点的结点)的信息结合了起来。
以hihoCoder1068为例给出代码:
#include
#include
#include
#include
#include
#include
using namespace std;
int Hash[15];
struct node
{
int f,h,g;
int x,y;
char map[3][3];
friend bool operator
{
if(a.f==b.f) return a.g
return a.f>b.f;
}
};
node start;
bool vis[500000];
int to[4][2]={0,-1,0,1,-1,0,1,0};
int pos[][2]={{0,0},{0,1},{0,2},{1,0},{1,1},{1,2},{2,0},{2,1},{2,2}};
//判断是否有解
int check()
{
int i,j,k;
int s[20];
int cnt=0;
for(i=0;i<3;i++)
{
for(j=0;j<3;j++)
{
s[3*i+j]=start.map[i][j];
if(s[3*i+j]=='x') continue;
for(k=3*i+j-1;k>=0;k--)
{
if(s[k]=='x') continue;
if(s[k]>s[3*i+j]) cnt++;
}
}
}
if(cnt%2) return 0;
return 1;
}
//康托展开
int solve(node a)
{
int i,j,k;
int s[20];
int ans=0;
for(i=0;i<3;i++)
{
for(j=0;j<3;j++)
{
s[3*i+j]=a.map[i][j];
int cnt=0;
for(k=3*i+j-1;k>=0;k--)
{
if(s[k]>s[3*i+j]) cnt++;
}
ans=ans+Hash[i*3+j]*cnt;
}
}
return ans;
}
//计算h值,即曼哈顿距离
int get_h(node a)
{
int i,j;
int ans=0;
for(i=0;i<3;i++)
{
for(j = 0; j<3; j++)
{
if(a.map[i][j]=='x') continue;
int k=a.map[i][j]-'1';
ans+=abs(pos[k][0]-i)+abs(pos[k][1]-j);
}
}
return ans;
}
//启发式搜索
int bfs()
{
memset(vis,0,sizeof(vis));
priority_queue Q;
start.g=0;
start.h=get_h(start);
start.f=start.h;
vis[solve(start)]=true;
if(solve(start)==0) return 0;
Q.push(start);
node next;
while(!Q.empty())
{
node a=Q.top();
Q.pop();
int k_s=solve(a);
vis[k_s]=true;
for(int i=0;i<4;i++)
{
next=a;
next.x+=to[i][0];
next.y+=to[i][1];
if(next.x<0||next.y<0||next.x>2||next.y>2) continue;
next.map[a.x][a.y]=a.map[next.x][next.y];
next.map[next.x][next.y]='x';
next.g+=1;
next.h=get_h(next);
next.f=next.g+next.h;
int k_n=solve(next);
if(k_n==0) return next.g;
if(vis[k_n]) continue;
Q.push(next);
}
}
}
int main()
{
Hash[0]=1;
for(int i=1;i<=9;i++) Hash[i]=Hash[i-1]*i;
int t;
cin>>t;
for(int i=0;i
{
for(int i=0;i<3;i++)
{
for(int j=0;j<3;j++)
{
char a;
cin>>a;
start.map[i][j]=a;
if(a=='0')
{
start.map[i][j]='x';
start.x=i;
start.y=j;
}
}
}
if(!check())
{
cout<
}
else cout<
}
}