题解——八数码难题&A*算法
题解——八数码
题目(粘贴自洛谷)
题目描述
在3×3的棋盘上,摆有八个棋子,每个棋子上标有1至8的某一数字。棋盘中留有一个空格,空格用0来表示。空格周围的棋子可以移到空格中。要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为123804765),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变。
输入格式
输入初始状态,一行九个数字,空格用0表示
输出格式
只有一行,该行只有一个数字,表示从初始状态到目标状态需要的最少移动次数(测试数据中无特殊无法到达目标状态数据)
输入输出样例
输入 283104765 输出 4
思路
那么这个题主要的思维难度在于如何设计状态,很多人都会想到用一个3*3的数组来模拟这个9宫格,但是实际上是不可行的,因为我们没有办法去表示这个状态下与初始状态的移动步数(也许可以用哈希表,但博主不会)那么如何来设计我们的状态呢?其实题目给了我们提示:用一个9位数来表示这个并状态,与数组上上的位置一一对应。举个例子:
123740865
1 2 3
7 4 0
8 6 5
用9位数的话,可以很好的表示到了这个状态后与初始状态的移动步数是多少,因为9位数比较大,开数组浪费空间,我们可以选择用map,但是有人要问了:如果用9位数的话,我们如何去交换两个数的位置呢?其实如果是一个数组的话就比较容易转换的。相信读者也注意到了,这里的数组与我们9位数的状态之间是可以互相转化的,我们只需要稍微处理一下,编个函数也罢。
所以这个题的总体思路就是:用9位数作为状态传递,用数组去模拟交换!
A*
因为这个题用到了A*算法,所以这里就简单的讲讲A*是什么.
简单地说,我们平常的搜索算法多是没有目的的搜索,但是呢A*却是有目的的搜索,好比一个人在操场上,要去国旗旗杆,平常的搜索更像是一个瞎子,最坏的话要把操场每一个位置都找遍才能到达旗杆的位置,但是A*更像是一个正常人,一眼看见旗杆的位置,并朝那个方向走过去,很明显A*算法要比BFS或DFS便捷的多,事实也是如此:A*比平常的搜索算法快得多!,这个题也不例外。
那么如何使用A*呢
我们对每个点定义一个估价函数f(x)=g(x)+h(x),g(x)表示从起始点到x的实际代价,h(x)表示估计的从x到结束点的代价,并要求h(x)小于等于从x到结束点的实际代价,那么每次我们从可行点集合中找到f(x)最小的x,然后搜索他,这个过程可以用优先队列(即堆)实现,这样的话可以更快地到达结束点,并保证到达结束点时走的是最优路径,一般来说,h函数的选择决定了A*算法的效率,h函数与实际值的差距越小越好
如果不知道优先队列是什么,可以去我的博客中的关于STL的讲解中看看(包括上文的map)。
对于这个题map的选择有两种:1.不在应该在的位置上的数的个数;2.所用数距离其应在位置的曼哈顿距离和。
博主选择第一种(比较好实现)
那么,让我们来看看代码吧!
代码
我们分开来看
定义部分
库
#include<iostream>
#include<cstdio>
#include<algorithm>
#include<cstring>
#include<vector>//
#include<queue>//
#include<map>//
#include<sstream>
#define N 100001
#define ll long long
using namespace std;
这里不用细说,但注意一下,带"//"的,都与STL有关,有兴趣的读者可以自行浏览我的讲STL的博客(稍微打下广告)
这里有一个define,主要功能是替换。也就是下面的long long 下面都可以打成ll了
map+ans
ll ans;
map<ll,int> f;//f=g+h
map<ll,bool> vis;
这里呢有一个ans,用来储存答案,两个map,f是A*中的f函数,(f=g+h),vis用来判重,
优先队列
struct rode{
int fh;
ll zt;
ll g;
rode() { };
rode(int fh,ll zt,ll g) : fh(fh),zt(zt),g(g) { };
};
struct cmp{
bool operator () (rode a,rode b)
{
return a.fh>b.fh;
}
};
priority_queue<rode,vector<rode>,cmp> q;
这里有一个rode结构体,fh是这个状态下的f值(A*),zt是这个状态,g是原始状态到这个状态移动所用的步数。在这个结构体中有有两个函数,叫做构造函数是在定义中使用的,在这个题中的意义就是在我们往下面这个优先队列里输进数据时能扣简便些。如果你不清楚哪里简便,一会看见dfs部分,你就知道了。
A*并不是独立存在的,这个算法会依附于其他的搜索算法,并加快其速度。
至于结构体cmp和重载运算符operator,博主讲STL博客中略有介绍,但如果有读者对重载运算符operator感兴趣,可以自行上网查阅,博主只是背了下来。在这里,结构体cmp的作用是让这个优先队列q其中元素的排列顺序是fh越小的越靠前,是的你没看错,是小。这可以被认为是一个小根堆。
至于最后一行是优先队列的定义形式,这里不在详讲。
其它
ll qishi,mubiao=123804765;
int a[4][4];
const int fx[]={0,1,-1,0,0};
const int fy[]={0,0,0,1,-1};
这里理解起来就比较简便了,qishi就是原始状态,即“起始”,mubiao即“目标”,是目标状态。下面那个数组是用来模拟9宫格的。下面的程序将实现这二者之间的转化。
而下面这两个数组,相信打过搜索的读者都比较熟悉,这里不多解释。
次要函数部分
就是相对容易一些的函数
转化1
inline ll zhuanhua1(ll zhuang,ll& x_0,ll& y_0)
{
for(int i=3;i>=1;i--)
{
for(int j=3;j>=1;j--)
{
a[i][j]=zhuang%10;
zhuang/=10;
if(a[i][j]==0)
{
x_0=i;
y_0=j;
}
}
}
}//over
这里的inline先不用去管他,我听别人说这个能减少时间,
但博主也不知道是什么干什么用的
这个函数实现了9位数状态到数组的转化过程。
常数中zhuang就是我们设计的状态,至于常数x_0,y_0,你也许已经知道这是什么意思,他们是0的位置的“横纵坐标”。
这里的“&”是用来返回数值的,一般来说,在运用这个函数时的变量与这个函数的变量之间没有关系,但这个符号实现了把数值返回。例如在我的主函数中:
zhuanhua1(zhuang,dx,dy);
在运行完毕后,dx,dy的值将会是0的位置的横纵坐标。
这里关于这个符号,有兴趣的朋友可以自行上网浏览。
至于函数内部吗,很好理解。相信各位读者也能看懂。如果仍有不懂的问题,请各位读者自行探索。
转化2
ll zhuanhua2()
{
ll resu=0;
for(int i=1;i<=3;i++)
{
for(int j=1;j<=3;j++)
{
resu+=a[i][j];
resu*=10;
}
}
resu/=10;
return resu;
}//over
这个函数实现了由数组到9位数状态的转化过程
其实与转化1是相反的。
这里请千万不要忘了最后除以10,因为循环结束后resu是一个10位数。
h函数
int h(ll qi,ll mu)
{
int an=0;
ll q=qi,m=mu;
while(q>0)
{
int i=q%10,j=m%10;
if(i!=j) an++;
q/=10;m/=10;
}
return an;
}//over
这就是A*算法中的重中之重,h函数,h函数的选择将直接决定到这个程序运行结果的方方面面。
刚刚在思路里讲了,博主选的h函数是不在应在位置上的数的个数,这里qi常数是目前状态,mu是目标状态,然后比较他们的每一位,如果不同的话an++,这便是这个函数,没有我们想象中的那么复杂。最后返回an。
主要函数部分
dfs+A*
ll dfs(ll zhuang)
{
if(zhuang==mubiao)
{
//return g;
return q.top().g;
}
ll dx,dy;
zhuanhua1(zhuang,dx,dy);
int gb=q.top().g;
q.pop();
for(int i=1;i<=4;i++)
{
int x,y;
x=dx+fx[i];
y=dy+fy[i];
if(x<1||y<1||x>3||y>3) continue;
swap(a[x][y],a[dx][dy]);
ll newz;
newz=zhuanhua2();
f[newz]=h(newz,mubiao)+gb+1;
if(!vis[newz]) q.push(rode(f[newz],newz,gb+1));
swap(a[dx][dy],a[x][y]);
}
int next=q.top().zt;
vis[next]=1;
return dfs(next);
vis[next]=0;
}
这里的常数是zhuang,也就是目前搜索到的状态。
这里如果目前状态已经是目标状态,那么优先队列队头元素的g就是正解。
至于原因嘛,可以这么理解,当目前状态和目标状态一样时,h应该是0,而这时f=g+h=g,因为优先队列是f越小的越往前,所以这里的f自然是最小。其余的f都大,f=g+h,而h还实际上小于实际值,加了个比实际值小的还大,更别提加的是实际值了。这里也说明了为什么h函数要小于实际值。
然后定义了dx,dy。调用函数zhuanhua1,这是数组a中已经至目前状态所对应的那个“9宫格”,这时gb存储一下队首函数的g值(因为下面要用),并弹出队首元素。
下面有一个循环,枚举是哪个位置上的数与0这个位置交换,x,y,即这个位置的横纵坐标,下面紧接着判断是否越界,如果没有的话交换这两个位置的值,并调用zhuanhua2,把这个数组对应的9位数状态输入到newz(即new zhuangtai,博主比较喜欢用这种方式定义变量)去,接着如果这个点没有被搜索过,就把newz的的f值算出来,就等于h函数加gb+1(因为这里再次移动,步数又加1)。再把这个newz的f值,它本身,和它的g值,扔进队列。
这里构造函数的便利就显现了出来,你不用再去构建一个结构体,而可以直接实现入队操作。
循环最后,把两个值交换回来,不影响另外的决策。
循环结束后,取出f值最小的那个状态,并把它的vis标记上,然后搜索他,最后回溯。
主函数
int main()
{
cin>>qishi;
f[qishi]=h(qishi,mubiao);
q.push(rode(f[qishi],qishi,0));
ans=dfs(qishi);
cout<<ans;
return 0;
}
这就比较好懂了,输入qishi值,因为qishi的g值为0,多以这里不用去管,光看h就可以了,然后把它入队,然后再搜索,然后再输出,然后。。。就结束了。
结束
那么这篇题解到这里就结束了,如果觉得还可以,请点个推荐,如果对哪里有意见回批评、建议,请在评论区留言。谢谢大家的观看!