采用最简单的宽搜做法,用一个hash表保存已经搜索过的节点。状态值使用一个int,每三位对应序列中的一个数,最后五位存放0的位置,0和8都用000表示。在C++中会超时,在G++中9232K 782MS ,可以说效率是比较低的。
使用双向bfs,可以大大提高效率。注意我这里双向广度的循环是逐层的。我原来以为,如果不用逐层,两个搜索队列的层数可能不平衡,效率可能会降低,其实不逐节点扩展速度也不差。每次扩展完一层后都在队列中加个0标志。以下是双向bfs实现,结果用了47ms。
最有意思的是A*算法。A*算法的写法与一般的宽搜没什么区别,只是它的队列不是顺序的,而是优先队列。遍历时每次取优先级最高的节点。
优先级怎么确定呢?通常我们使用估值函数f=g+h。g是从难题节点到当前节点的遍历距离,h是当前节点到顺序节点(1 2 3 4 5 6 7 8 x)的距离估计。如果令h=0,就退化到普通的bfs。距离估计通常有两种:第一种直接比较两个节点的差异个数,第二种计算两个节点的曼哈顿距离。第二种方法使用效果较好,而且不包括数字0(即空块)的曼哈顿距离更好。估值函数中g的存在可以限制求出来的解不会太深,h可以加快搜索效率。如果给g和h加上权值,我们可以认为是求好的解(步数少)和快的解(搜索快,扩展节点少)的权衡。
还要注意g是应该在搜索过程中更新的,即发现扩展的节点比原来步数更少,应该更新。更新时不能直接修改优先队列中的数值,还应通知优先队列已经修改了,以便优先队列保持堆结构。这时判重的哈希表的节点中应该保存g。
如果完全忽略g,则哈希表中只需保存状态和方向。而队列中保存节点的曼哈顿距离。另外发现在求优先级时如果曼哈顿距离相等,再比较两者数字0处(即空块)的曼哈顿距离大小,小的优先会进一步减少扩展节点个数。如果数字0处曼哈顿距离仍相等,再比较二者的g大小,小的g优先,扩展节点个数会有非常微弱的减小。如果原来不计算g,为了微弱的性能提升,现在要比较还要维护,有点得不偿失。随机测试一千个序列,得到的平均扩展节点个数如下:
双向宽度优先搜索 661
仅比较曼哈顿距离 236
还比较数字0出的曼哈顿距离 173
还比较g 168
以下是比较曼哈顿距离再比较数字0出的曼哈顿距离的做法,在POJ上提交,620K 0MS。很奇怪的是代码提交到hdu1043和ustc1012都是错误的,以至于后来自己写了个special judge,对9!的全排列一一做了测试,也没发现问题。