这些天一直在研究8数码问题,用C++实现了A*。并且用了C++的模板使得它也能够处理15数码的问题。但是15数码的问题的难度超乎了我的想象。A*不管用了。一个更好方案是使用IDA*算法。前几天看了篇论文,发现了一个增强版的IDA*算法。所以决定使用这个威力加强版IDA*算法来处理15数码的问题。在这之前,我决定写一下处理8数码问题的一些心得。
我的8数码问题的实现涉及到的一些东西。我简要地把它们列了一下:
1. 8数码问题的计算机表示
使用二维数组来表示,具体下文会说到。
2. 曼哈顿距离 定义请看百度百科
用来计算棋盘到棋盘的距离。棋盘1到棋盘2的距离就是棋盘1中每个数字的位置与棋盘2中对应数字的位置的曼哈顿距离的总和。
3. A*算法
A*算法的大致思想是先根据初始棋盘随便走几步来生成一些棋盘,然后依据启发函数找到最接近最终棋盘的棋盘B,再从棋盘B出发生成些棋盘,用启发函数找到最接近最终棋盘的那个。以此类推直到找到的棋盘就是最终棋盘。
4.stl中的priority_queue和set
C++标准模板库中的两个容器,优先队列和集合。
5.C++函数对象
用来定制stl中的容器。
6.C++模板
使用模板的好处就是一个类可同时应用于8数码和15数码问题。
我定义了模板类SlidingPuzzleNode<NP>来表示每个棋盘节点。每个棋盘应该具有如下的属性:
1. 棋盘的表示
我定义了一个二维数组来表示棋盘。上图中的3X3的棋盘可以这样定义char board[3][3]={
{1,7,8},
{6,5,2},
{4,3,0}
};
棋盘中的空格用0来表示。
2. 启发函数的计算
启发函数f由两部分构成。到最终棋盘的距离加上到初始棋盘的最短距离。到最终棋盘的距离使用曼哈顿距离,到初始棋盘的距离是从初始棋盘到当前棋盘走的最少步数。
3. 下一步走的棋盘节点
我定义了vector<SlidingPuzzleNode<NP>*> neighbors;来存放下步走的棋盘。
4.上一步棋盘节点
我定义了SlidingPuzzleNode* parent;来表示上一步的棋盘节点。
5. 区分每个棋盘的ID
下文中介绍。
6. 判断是否为目标棋盘
用棋盘的ID判断。
A*算法本身不难理解。它有两个容器openset和closedset。openset存放待考察的棋盘,closedset存放考察过的棋盘。每次从openset中取出f()最小的棋盘,(f()是启发函数,它由g()和h()构成。g()是初始棋盘到当前棋盘走的最小步数,h()是当前棋盘到目标棋盘的启发式距离。h()的值越小,它离目标棋盘的距离越近。f()值最小表示它可能是初始棋盘到目标棋盘的最短路径中的某个节点。)如果它是最终棋盘则算法结束。否则放入closedset中。再得到它的下一步棋盘k,如果k已经在closedset中,表明这个棋盘k已经考察过了。如果k已在openset中出现过并且k的g()比openset中的g()小,更新openset中的g()值为k的g()的值。如果这些条件都不满足,直接放入openset中。wiki上的伪代码:
可以看到A*算法需要对openset和closedset这两个集合进行频繁地插入和查找操作。所以openset和closedset使用哪种数据结构来表示非常重要。启发函数的计算和取得下一步的棋盘等操作由棋盘节点SlidingPuzzleNode类自己来完成,在我自己实现的A*算法中就不需要操心了,这是面向对象的好处啊。下面的openset和closedset的表示:
由于openset的作用只是每次取f()最小的棋盘,所以使用stl的优先队列priority_queue表示。priority_queue使用最大堆来实现,每次返回权值最大的元素。为此需要提供一个函数对象Greator把它变为最小堆。Greator很简单,源码如下:
priority_queue使用vector为底层容器,vector的一个很大特点就是当它已满时,会开辟一个更大的空间,把原来的元素通通复制过去。所以priority_queue存放的元素最好是复制代价很小的值类型或指针。所以openset的定义是这样子的:
closedset由于要进行频繁地查找操作,我使用stl中的set表示。set使用红黑树来实现,每次查找的代价为O(lgn)。为了使用set,还必须提供<操作符。我使用函数对象Less,源码如下:
closedset的定义:
hash()函数返回每个棋盘的ID,两个不同的棋盘不可能返回相同的ID。为了达到这个目的,hash()函数的实现为取每个棋盘的数字序列。例如这个棋盘的数字序列为123804765。
有了这些就可以实现A*算法了:
完整的源代码下载