八数码问题详解.docx
《八数码问题详解.docx》由会员分享,可在线阅读,更多相关《八数码问题详解.docx(15页珍藏版)》请在冰豆网上搜索。
八数码问题详解
八数码问题详解(用bfs实现)
注:
下面要介绍的方法是LRJ大神首创的,笔者只是在巨人的肩膀上归纳总结了一下,有错误的还希望众大牛指点,本人定将不胜感激。
八数码问题也称为九宫问题。
在3×3的棋盘,摆有八个棋子,每个棋子上标有1至8的某一数字,不同棋子上标的数字不相同。
棋盘上还有一个空格,与空格相邻的棋子可以移到空格中。
要求解决的问题是:
给出一个初始状态和一个目标状态,找出一种从初始转变成目标状态的移动棋子步数最少的移动步骤。
所谓问题的一个状态就是棋子在棋盘上的一种摆法。
棋子移动后,状态就会发生改变。
解八数码问题实际上就是找出从初始状态到达目标状态所经过的一系列中间过渡状态。
如图:
对这道题用bfs解决个人认为是个不错的方法,首先因为它有九个格子,那么便可以用state数组记录他的的九个格子的数值,其中空格为0,后者由于它只有九个数,因此共有9!
=362880种可能性,用bfs解决较快。
下面给出bfs实现的三种代码(这里的三种指的是对是否访问过已知节点的三种判断方法):
1.//用一套排列的编码和解码函数解决同一状态的再次访问
//用统一的编码与解码函数避免同种状态的再次出现
#include
#include
#include
#include
#definelen362888//状态共有362880种,数组稍微开大点
#definele9//每种状态有9个数据,也可看为每种状态下又有9种状态
typedefintstate[le];//状态:
表示九个格子
statest[len],goal;//st为状态数组goal为目标数组
intdis[len],fact[le],head[len],vis[len],der[4][2]={{-1,0},{1,0},{0,-1},{0,1}};//dis为每种状态的已走的步骤//der为方向:
上,下,左,右
voidencode(){//编码
inti;
for(i=fact[0]=1;ifact[i]=fact[i-1]*i;
}
intdecode(ints){//解码
inti,j,code,cnt;
for(i=code=0;ifor(cnt=0,j=i+1;jif(st[s][i]>st[s][j])
cnt++;
code+=cnt*fact[8-i];
}
if(vis[code])return0;
elsereturnvis[code]=1;
}
intbfs(){
intfront=1,rear=2,i,x,y,z,nx,ny,nz;
encode();
while(frontstate&s=st[front];
if(memcmp(s,goal,sizeof(s))==0)//对front状态和目标状态进行比较
returnfront;
for(i=0;iif(s[i]==0)
break;
x=i/3;y=i%3;z=i;//记录空的格子的行标,列表,和所在位置,这里的位置按照从左到右从上到下依次递增
for(i=0;i<4;i++){//按照上,下,左,右四个方向进行搜索
nx=x+der[i][0];
ny=y+der[i][1];
nz=nx*3+ny;
if(nx>=0&&nx<3&&ny>=0&&ny<3){
state&t=st[rear];
memcpy(&t,&s,sizeof(s));//记录此时的状态即九个格子的数值
t[z]=s[nz];t[nz]=s[z];
dis[rear]=dis[front]+1;
if(decode(rear))//判断st[rear]这种状态是否已经出现过
rear++;
}
}
front++;
}
return0;
}
intmain(void){
intncase,i,oj;
scanf("%d",&ncase);
while(ncase--){
memset(head,0,sizeof(head));
memset(vis,0,sizeof(vis));
for(i=0;ifor(i=0;ioj=bfs();
if(oj)
printf("%d\n",dis[oj]);
else
puts("-1");
}
return0;
}
2.//用hash避免同一状态的再次访问
在讲这种方法之前建议读者先看一下算法导论中有关hash的介绍
为了方便自己以后看的时候方便,先写一下有关hash的内容
Hash表解决冲突
三种hash函数:
first:
除法散列函数:
h(k)=kmodm(m为所选的余数,最好选接近装载因子α=n/m,但又远离2的k次幂的质数)
second:
乘法散列法函数:
看图:
具体代码实现下面有介绍
third:
全域散列函数
ha,b(k)=((ak+b)modp)modm(p>m且p和m都为质数)
//用链表实现hash
#include
#include
#include
#include
#definelen362888//状态共有362880种,数组稍微开大点
#definele9//每种状态有9个数据,也可看为每种状态下又有9种状态
typedefintstate[le];//状态:
表示九个格子
statest[len],goal;//st为状态数组goal为目标数组
intdis[len],der[4][2]={{-1,0},{1,0},{0,-1},{0,1}};//dis为每种状态的已走的步骤//der为方向:
上,下,左,右
typedefstructnode{
intv;
structnode*next;
}ore;
ore*head[len];//这里的head为hash表
ore*create_new_node(){
ore*p;
p=(ore*)calloc(1,sizeof(ore));
p->next=NULL;
returnp;
}
//此处为哈希函数,不理解的建议看一下算法导论,下面用3种hash函数实现
//first:
除法散列法
inthash(state&s){
inti,num,m=372001;//m为所选的余数,最好选接近装载因子α=n/m,但又远离2的k次幂的质数
for(i=num=0;inum=num*10+s[i];
returnnum%m;
}
//second:
乘法散列法
inthash(state&s){
inti,num;
longlongk,w=32,ss,r0,p=14,ans;//这里的w为需要截取的位数//p为要截取的数字长度
constdoubleA=(sqrt(5.)-1)/2;
for(i=num=0;inum=num*10+s[i];
k=(longlong)num;
ss=(longlong)(A*(1LL<r0=k*ss%(1LL<ans=r0>>(w-p);
returnans;
}
//third:
全域散列hashfunction
inthash(state&s){
inti,num;
longlonga=3,b=4,m=350001,p=360001,k,ans;
for(i=num=0;inum=num*10+s[i];
k=(longlong)num;
ans=(a*k+b)%p%m;//此处为全域散列函数
returnans;
}
//以上的三种hashfunction选取一种即可
boolfind(ints){
inth;
ore*u,*p;
h=hash(st[s]);//通过hashfunction计算出hash值,并将该元素定义为head数组的下标
u=create_new_node();
if(!
head[h])//如果head[h]未创建,即未访问过,则创建一个新节点
head[h]=create_new_node();
u=head[h]->next;//u指向head[h]的下一个元素
while(u){
if(memcmp(st[u->v],st[s],sizeof(st[s]))==0)//如果找到memcmp(st[u->v],st[s],sizeof(st[s]))==0的数据项则说明该节点已经访问过
returnfalse;
u=u->next;//访问下一个节点//原理看下面的说明
}
p=create_new_node();//创建一个新节点
p->next=head[h]->next;//用头插法在散列表中插入新的节点
head[h]->next=p;
p->v=s;
returntrue;
}
intbfs(){
intfront=1,rear=2,i,x,y,z,nx,ny,nz;
while(frontstate&s=st[front];
if(memcmp(s,goal,sizeof(s))==0)//对front状态和目标状态进行比较
returnfront;
for(i=0;iif(s[i]==0)
break;
x=i/3;y=i%3;z=i;//记录空的格子的行标,列表,和所在位置,这里的位置按照从左到右从上到下依次递增
for(i=0;i<4;i++){//按照上,下,左,右四个方向进行搜索
nx=x+der[i][0];
ny=y+der[i][1];
nz=nx*3+ny;
if(nx>=0&&nx<3&&ny>=0&&ny<3){
state&t=st[rear];
memcpy(&t,&s,sizeof(s));//记录此时的状态即九个格子的数值
t[z]=s[nz];t[nz]=s[z];
dis[rear]=dis[front]+1;
if(find(rear))//判断st[rear]这种状态是否已经出现过
rear++;
}
}
front++;
}
return0;
}
intmain(void){
intncase,i,oj;
scanf("%d",&ncase);
while(ncase--){
memset(head,0,sizeof(head));
for(i=0;ifor(i=0;ioj=bfs();
if(oj)
printf("%d\n",dis[oj]);
else
puts("-1");
}
return0;
}
//基于链表的用数组实现hash
#include
#include
#include
#include
#definelen362888//状态共有362880种,数组稍微开大点
#definele9//每种状态有9个数据,也可看为每种状态下又有9种状态
typedefintstate[le];//状态:
表示九个格子
statest[len],goal;//st为状态数组goal为目标数组
intdis[len],head[len],next[len],der[4][2]={{-1,0},{1,0},{0,-1},{0,1}};//dis为每种状态的已走的步骤//head为哈希表//next为链表//der为方向:
上,下,左,右
//此处为哈希函数,不理解的建议看一下算法导论,下面用3种hash函数实现
//first:
除法散列法
inthash(state&s){
inti,num,m=372001;//m为所选的余数,最好选接近装载因子α=n/m,但又远离2的k次幂的质数
for(i=num=0;inum=num*10+s[i];
returnnum%m;
}
//second:
乘法散列法
inthash(state&s){
inti,num;
longlongk,w=32/*这里的w为需要截取的位数*/,ss,r0,p=14/*p为要截取的数字长度*/,ans;
constdoubleA=(sqrt(5.)-1)/2;
for(i=num=0;inum=num*10+s[i];
k=(longlong)num;
ss=(longlong)(A*(1LL<r0=k*ss%(1LL<ans=r0>>(w-p);
returnans;
}
//third:
全域散列hashfunction
inthash(state&s){
inti,num;
longlonga=3,b=4,m=350001,p=360001,k,ans;
for(i=num=0;inum=num*10+s[i];
k=(longlong)num;
ans=(a*k+b)%p%m;//此处为全域散列函数
returnans;
}
//以上的三种hashfunction选取一种即可
boolfind(ints){
inth,u;
h=hash(st[s]);//通过hashfunction计算出hash值,并将该元素定义为head数组的下标
u=head[h];//通过u获得head[h]的值
while(u){//如果前面已经访问过该项数据,则说明数据已经插入该项所对应的next数组中,则继续访问
if(memcmp(st[u],st[s],sizeof(st[s]))==0)//如果找到memcmp(st[u],st[s],sizeof(st[s]))==0的数据项则说明该节点已经访问过
returnfalse;
u=next[u];//访问下一个节点//原理看下面的说明
}
//这里的next其实是一个个链表的集合所组成的数组,不用链表的原因是应为链表的创建需要耗时,而且还要有多余的空间存储指针
next[s]=head[h];//这里的原理实际上是基于链表的头插法
head[h]=s;
returntrue;
}
intbfs(){
intfront=1,rear=2,i,x,y,z,nx,ny,nz;
while(frontstate&s=st[front];
if(memcmp(s,goal,sizeof(s))==0)//对front状态和目标状态进行比较
returnfront;
for(i=0;iif(s[i]==0)
break;
x=i/3;y=i%3;z=i;//记录空的格子的行标,列表,和所在位置,这里的位置按照从左到右从上到下依次递增
for(i=0;i<4;i++){//按照上,下,左,右四个方向进行搜索
nx=x+der[i][0];
ny=y+der[i][1];
nz=nx*3+ny;
if(nx>=0&&nx<3&&ny>=0&&ny<3){
state&t=st[rear];
memcpy(&t,&s,sizeof(s));//记录此时的状态即九个格子的数值
t[z]=s[nz];t[nz]=s[z];
dis[rear]=dis[front]+1;
if(find(rear))//判断st[rear]这种状态是否已经出现过
rear++;
}
}
front++;
}
return0;
}
intmain(void){
intncase,i,oj;
scanf("%d",&ncase);
while(ncase--){
memset(head,0,sizeof(head));
memset(next,0,sizeof(next));
for(i=0;ifor(i=0;ioj=bfs();
if(oj)
printf("%d\n",dis[oj]);
else
puts("-1");
}
return0;
}
3.用stl集合避免重复访问同一状态
//用stl避免同一状态重复出现
#include
#include
#include
#include
#include
#include
usingnamespacestd;
#definelen362888//状态共有362880种,数组稍微开大点
#definele9//每种状态有9个数据,也可看为每种状态下又有9种状态
typedefintstate[le];//状态:
表示九个格子
statest[len],goal;//st为状态数组goal为目标数组
intdis[len],head[len],der[4][2]={{-1,0},{1,0},{0,-1},{0,1}};//dis为每种状态的已走的步骤//der为方向:
上,下,左,右
structcmp{
booloperator()(inta,intb)const{
returnmemcmp(&st[a],&st[b],sizeof(st[a]))<0;
}
};
setvis;
voidinit_lookup_table(){
vis.clear();
}
inttry_to_insert(ints){
if(vis.count(s))return0;
vis.insert(s);
return1;
}
intbfs(){
intfront=1,rear=2,i,x,y,z,nx,ny,nz;
init_lookup_table();
while(frontstate&s=st[front];
if(memcmp(s,goal,sizeof(s))==0)//对front状态和目标状态进行比较
returnfront;
for(i=0;iif(s[i]==0)
break;
x=i/3;y=i%3;z=i;//记录空的格子的行标,列表,和所在位置,这里的位置按照从左到右从上到下依次递增
for(i=0;i<4;i++){//按照上,下,左,右四个方向进行搜索
nx=x+der[i][0];
ny=y+der[i][1];
nz=nx*3+ny;
if(nx>=0&&nx<3&&ny>=0&&ny<3){
state&t=st[rear];
memcpy(&t,&s,sizeof(s));//记录此时的状态即九个格子的数值
t[z]=s[nz];t[nz]=s[z];
dis[rear]=dis[front]+1;
if(try_to_insert(rear))//判断st[rear]这种状态是否已经出现过
rear++;
}
}
front++;
}
return0;
}
intmain(void){
intncase,i,oj;
scanf("%d",&ncase);
while(ncase--){
memset(head,0,sizeof(head));
for(i=0;ifor(i=0;ioj=bfs();
if(oj)
printf("%d\n",dis[oj]);
else
puts("-1");
}
return0;
}