【八数码问题】八数码问题 - Vijos
在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
【分析】
题目读完第一感觉是和求解最短路径问题类似,考虑使用BFS,状态很好找,每次移动空格就会形成一种新的状态,例如:
一、状态如何表示?
1.每个状态都用3*3的数组表示,但是BFS中需要入队出队,比较麻烦而且空间占用较大
2.状态压缩,采用一个整数保存状态的数字序列,例如状态1表示为283104765,状态2表示为203184765
二、如何判重?
1.如果空间允许,开一个876543210大小的bool数组,某个序列出现就将数组值置为1;但是竞赛中一般都是限制128M(大约10000000),这个数组开不下来。
2.虽然状态范围是012345678--876543210,但是中间真正有效的只有9!=362800,因为数字不可能出现重复;因此可以考虑开一个数组大小为9!整型数组A和bool数组B,然后生成0-8这9个数码的全排列并按照升序或者降序存入数组中,要判断某个状态(一种排列方式)是否出现过,直接通过二分查找的方式找到该排列在A中的下标i,然后查看数组B[i]为true还是false;如果为true则出现过,如果为false则将状态入队,并设置B[i]=true;
3.其实从方案2中我们已经看到,判重的实质就是建立状态数字串(一个int数据)和是否出现(一个bool数据)之间的联系,而STL中刚好提供了map<key,value>这样一种容器,我们可以将状态数字串作为key,是否出现作为value直接建立起状态--是否出现的联系。
4.使用hash判重,将状态数字串通过某种映射f(x)从012345678--876543210这样一个大集合,映射到128M范围之内;这里采用简单的hash,取模一个大质数,只要这个质数大于9!即可;当然这里可能出现冲突,也就是key1!=key2但是f(key1)==f(key2),hash算法只能减少冲突不能避免冲突。这里如何减少冲突呢?挂链表,当key1!=key2但是f(key1)==f(key2),则将key2挂到key1后面;当然这里如果使用康托展开可以完美一一映射而不冲突,但是我不会(~^~)。
三、搜索方法的选择
搜索方法大方向肯定是BFS,只是在对BFS基础上还可以更优化,这里给出三种搜索方式,三种搜索方式和上面的三种判重组合起来就有9种解法了(哈哈哈);
1.BFS(广度优先搜索)
广搜遍历:
(1)用户输入八数码的初始状态后,将状态压缩为一个整数。
(2)系统将此数据在全排列中的位置记为被访问(用于判重),同时将次数据储存入结构体数组中。
(3)首先判断数据是否已经是目标状态,若是,则结束函数。
(4)若不是,系统将数据转化为二维数组,并记录八数码中0在数组中的位置,然后在数组中依次向四个方向扩展。若越界则直接进行下个方向的扩展,其余继续扩展。
(5)扩展过程中将0与扩展方向的数据交换,转化为整数并判断此数据在全排列中的位置是否记为已访问,若未访问过,将此数据记录入结构体数组中,并判断该数据是否为目标状态。否则跳出此次扩展,最后数组交换为扩展前的数组,继续向其他方向扩展。
(6)该数据的四个方向扩展完成后,继续扩展结构体数组中下一个数据,重复(4)(5)过程,直至达到目标状态或不能继续扩展的状态。
(7)若可以达到目标状态,则通过子节点递归寻找父节点,矩阵输出每个节点所储存的数据,若不能则结束
2.DBFS(双向广度优先搜索)
双向广度优先,两个队列,一个从起点开始扩展状态,另一个从终点开始扩展状态;如果两者相遇,则表示找到了一条通路,而且是最短的通路。双向广度优先可以大大提高效果,而且可以减少很多不必要的状态扩展。盗个图来说明一下,其中的阴影部分为减少的扩展状态
3.A*(启发式搜索)
启发式搜索,关键是启发策略的制定,一个好的启发式策略可以很快的得到解,如果策略不好可能会导致找不到正确答案。
在BFS搜索算法中,如果能在搜索的每一步都利用估价函数f(n)=g(n)+h(n)对Open表(队列)中的节点进行排序,则该搜索算法为A算法。由于估价函数中带有问题自身的启发性信息,因此,A算法又称为启发式搜索算法。
对启发式搜索算法,又可根据搜索过程中选择扩展节点的范围,将其分为全局择优搜索算法和局部择优搜索算法。
• 在全局择优搜索中,每当需要扩展节点时,总是从 Open 表的所有节点中选择一个估价函数值最小的节点进行扩展。其搜索过程可能描述如下:
• ( 1 )把初始节点 S0 放入 Open 表中, f(S0)=g(S0)+h(S0) ;
• ( 2 )如果 Open 表为空,则问题无解,失败退出;
• ( 3 )把 Open 表的第一个节点取出放入 Closed 表,并记该节点为 n ;
• ( 4 )考察节点 n 是否为目标节点。若是,则找到了问题的解,成功退出;
• ( 5 )若节点 n 不可扩展,则转到第 (2) 步;
• ( 6 )扩展节点 n ,生成子节点 ni ( i =1,2, …… ) ,计算每一个子节点的估价值 f( ni ) ( i =1,2, …… ) ,并为每一个子节点设置指向父节点的指针,然后将这些子节点放入 Open 表中;
• ( 7 )根据各节点的估价函数值,对 Open 表中的全部节点按从小到大的顺序重新进行排序;
• ( 8 )转第 (2) 步。
这里采用的启发式策略为:f(n) = g(n) + h(n),其中g(n)为从初始节点到当前节点的步数(层数),h(n)为当前节点“不在位”的方块数,例如下图中的h(n)=5,有很多博客中讲解的是不包含空格,我这里是包含了的,经测试只要前后标准一致,包不包含空格都一样。
四、代码实现
1.全排列+BFS
#include<cstdio>
#include<cstring>
#include<ctime>
char ans[11],start[10];
bool isUsed[11];
int changeId[9][4]={{-1,-1,3,1},{-1,0,4,2},{-1,1,5,-1},
{0,-1,6,4},{1,3,7,5},{2,4,8,-1},
{3,-1,-1,7},{4,6,-1,8},{5,7,-1,-1}
};//0出现在0->8的位置后该和哪些位置交换
const int M=400000;//9!=362800,因此数组开40W足够了
int num[M],len=0,des=123804765;//num存储所有排列,len表示排列的个数也就是9!,des为目的状态直接用整数表示便于比较
bool isV[M];//bfs时判断状态是否出现过;isV的下标和num的下标一一对应,表示某种排列是否出现过
//通过isV和num建立起某种排列的组合成的整数int和bool的关系,其实STL中有map实现了key-->value,用排列作为key,value用bool即可
int que[M][3];//0-->排列,1-->排列中0的位置,2-->步数
void swap(char *c,int a,int b){//交换字符串中的两个位置
char t=c[a];
c[a]=c[b];
c[b]=t;
}
void paiLie(int n,int k){//深搜产生0-8的全排列
for(int i=0;i<n;i++){
if(!isUsed[i]){
ans[k]=i+'0';
isUsed[i]=1;
if(k==n){//已经有n个转换存储
ans[k+1]='\0';
sscanf(ans+1,"%d",&num[len++]);
}
else
paiLie(n,k+1);
isUsed[i]=0;//回溯一步
}
}
}
int halfFind(int l,int r,int n){//二分查找
int mid=l+(r-l)/2;
if(num[mid]==n)return mid;
else if(l<r&&num[mid]>n)return halfFind(l,mid-1,n);
else if(l<r&&num[mid]<n) return halfFind(mid+1,r,n);
return -1;
}
int bfs(int n,int p){
int head=0,tail=1,temp;//head队头,tail队尾
que[head][0]=n,que[head][1]=p,que[head][2]=head;//初始状态保存到对头,并设置当前步数为0
while(head!=tail){//队列不为空则继续搜索
char cur[10];//用于保存当前状态的字符串
int pos=que[head][1];//当前状态中0的位置
sprintf(cur,"%09d",que[head][0]);//int-->char*这里的09d至关重要,否则算不出答案
for(int i=0;i<4;i++){//扩展当前的状态,上下左右四个方向
int swapTo=changeId[pos][i];//将要和那个位置交换
if(swapTo!=-1){//-1则不交换
swap(cur,pos,swapTo);//交换0的位置得到新状态
sscanf(cur,"%d",&temp);//新状态转换为int保存到temp
if(temp==des)//如果是目标状态则返回当前状态的步数+1
return que[head][2]+1;
int k=halfFind(0,len,temp);//没有返回就查找当前排列的位置,将查出来的下标作为isV的下标
if(!isV[k]){//如果 没有出现过,则将这个新状态进队
que[tail][0]=temp,que[tail][1]=swapTo,que[tail][2]=que[head][2]+1;
tail++;
isV[k]=1;
}
swap(cur,pos,swapTo);//一个新状态处理完了一定要记得将交换的0交换回来
}
}
head++;
}
}
int main(){
int n,i=-1,count=0;
paiLie(9,1);//先将0-8的全排列按照升序产生出来存入num数组
scanf("%s",start);//输入初始状态
while(start[++i]!='0');//查找初始状态0的位置
sscanf(start,"%d",&n);//字符串转换为整数
//int s=clock();
if(n!=des)//判断输入状态是否就是目的状态
count=bfs(n,i);
printf("%d\n",count);
//printf("%.6lf",double(clock()-s)/CLOCKS_PER_SEC);
return 0;
}
2.Hash+BFS
#include<cstdio>
#include<cmath>
using namespace std;
char arr[10];
int changeId[9][4]={{-1,-1,3,1},{-1,0,4,2},{-1,1,5,-1},
{0,-1,6,4},{1,3,7,5},{2,4,8,-1},
{3,-1,-1,7},{4,6,-1,8},{5,7,-1,-1}};
const int M=2E+6,N=1000003;//362897;
int hashTable[M];//hashtable中key为hash值,value为被hash的值
int next[M];//next表示如果在某个位置冲突,则冲突位置存到hashtable[next[i]]
int que[N][3],des=123804765;
int hash(int n){
return n%N;
}
bool tryInsert(int n){
int hashValue=hash(n);
while(next[hashValue]){//如果被hash出来的值得next不为0则向下查找
if(hashTable[hashValue]==n)//如果发现已经在hashtable中则返回false
return false;
hashValue=next[hashValue];
}//循环结束hashValue指向最后一个hash值相同的节点
if(hashTable[hashValue]==n)//再判断一遍
return false;
int j=N-1;//在N后面找空余空间,避免占用其他hash值得空间造成冲突
while(hashTable[++j]);//向后找一个没用到的空间
next[hashValue]=j;
hashTable[j]=n;
return true;
}
void swap(char* ch,int a,int b){char c=ch[a];ch[a]=ch[b];ch[b]=c;}
int bfsHash(int start,int zeroPos){
char temp[10];
int head=0,tail=1;
que[head][0]=start,que[head][1]=zeroPos,que[head][2]=0;
while(head!=tail){
sprintf(temp,"%09d",que[head][0]);
int pos=que[head][1],k;
for(int i=0;i<4;i++){
if(changeId[pos][i]!=-1){
swap(temp,pos,changeId[pos][i]);
sscanf(temp,"%d",&k);
if(k==des)return que[head][2]+1;
if(tryInsert(k)){//插入新状态成功,则说明新状态没有被访问过
que[tail][0]=k;
que[tail][1]=changeId[pos][i];
que[tail][2]=que[head][2]+1;
tail++;
}
swap(temp,pos,changeId[pos][i]);
}
}
head++;
}
}
int main(){
int n,k;
scanf("%s",arr);
for(k=0;k<9;k++)
if(arr[k]=='0')break;
sscanf(arr,"%d",&n);
printf("%d",bfsHash(n,k));
return 0;
}