该文转载来自:
文章的第一部分首先分析了各种基本的搜索及其各自的特点。第二部分在基本搜索方法的基础上提出 一些更高级的搜索,提高搜索的效率。第三部分将搜索和动态规划结合,高效地解决实际问题,体现搜索的广泛应用性。第四部分总结全文。
第一部分 基本的搜索算法 一、回溯算法 回溯算法是所有搜索算法中最为基本的一种算法,其采用了一种“走不通就掉头”思想作为其控制结构,其相当于采用了先根遍历的方法来构造解答树,可用于找解或所有解以及最优解。 评价:回溯算法对空间的消耗较少,当其与分枝定界法一起使用时,对于所求解在解答树中层较深的问题 有较好的效果。但应避免在后继节点可能与前继节点相同的问题中使用,以免产生循环。 二、深度搜索与广度搜索 深度搜索与广度搜索的控制结构和产生系统很相似,唯一的区别在于对扩展节点选取上。由于其保留了所有的前继节点,所以在产生后继节点时可以去掉一部分重复 的节点,从而提高了搜索效率。这两种算法每次都扩展一个节点的所有子节点,而不同的是,深度搜索下一次扩展的是本次扩展出来的子节点中的一个,而广度搜索 扩展的则是本次扩展的节点的兄弟节点。在具体实现上为了提高效率,所以采用了不同的数据结构. 评价:广度搜索是求解最优解的一种较好的方法,在后面将会对其进行进一步的优化。而深度搜索多用于只要求解,并且解答树中的重复节点较多并且重复较难判断时使用,但往往可以用A*或回溯算法代替。 第二部分 搜索算法的优化(一) 一、双向广度搜索 广度搜索虽然可以得到最优解,但是其空间消耗增长太快。但如果从正反两个方向进行广度搜索,理想情况下可以减少二分之一的搜索量,从而提高搜索速度。 二、分支定界 分支定界实际上是A*算法的一种雏形,其对于每个扩展出来的节点给出一个预期值,如果这个预期值不如当前已经搜索出来的结果好的话,则将这个节点(包括其子节点)从解答树中删去,从而达到加快搜索速度的目的。 三、A*算法 A*算法中更一般的引入了一个估价函数f,其定义为f=g+h。其中g为到达当前节点的耗费,而h表示对从当前节点到达目标节点的耗费的估计。其必须满足两个条件: 1. h必须小于等于实际的从当前节点到达目标节点的最小耗费h*。 2. f必须保持单调递增。 A*算法的控制结构与广度搜索的十分类似,只是每次扩展的都是当前待扩展节点中f值最小的一个,如果扩展出来的节点与已扩展的节点重复,则删去这个节点。如果与待扩展节点重复,如果这个节点的估价函数值较小,则用其代替原待扩展节点。 对A*算法的改进--分阶段A*. 当A*算法出现数据溢出时,从待扩展节点中取出若干个估价函数值较小的节点,然后放弃其余的待扩展节点,从而可以使搜索进一步的进行下去。 四、A*算法与回溯的结合(IDA*) 这是A*算法的一个变形,很好综合了A*算法的人工智能性和回溯法对空间的消耗较少的优点,在一些规模很大的搜索问题中会起意想不到的效果。它的具体名称 是 Iterative Deepening A*, 1985年由Korf提出。该算法的最初目的是为了利用深度搜索的优势解决广度A*的空间问题,其代价是会产生重复搜索。归纳一下,IDA*的基本思路 是:首先将初始状态结点的H值设为阈值maxH,然后进行深度优先搜索,搜索过程中忽略所有H值大于maxH的结点;如果没有找到解,则加大阈值 maxH,再重复上述搜索,直到找到一个解。在保证H值的计算满足A*算法的要求下,可以证明找到的这个解一定是最优解。在程序实现上,IDA* 要比 A* 方便,因为不需要保存结点,不需要判重复,也不需要根据 H值对结点排序,占用空间小。 下面,以一个具体的实例来分析比较上述几种搜索算法的效率等问题。 在scu online judge(http://cs.scu.edu.cn/acm)上有这么一道题目:这就是古老而又经典的15数码难题:在4*4的棋盘上,摆有15个棋 子,每个棋子分别标有1-15的某一个数字。棋盘中有一个空格,空格周围的棋子可以移到空格中。现给出初始状态和目标状态,要求找到一种移动步骤最少的方 法。 看到这个题目,会发觉几乎每个搜索算法都可以解这个问题。而事实确实如此。 首先考虑深度优先搜索,它会遍历这棵解答树。这棵解答树最多可达16!个节点,深度优先搜索必须全部遍历后,才能从所有解中选出最小的一个做为答案,其代价是非常巨大的。 其次考虑广度深度优先搜索,这不失为一个好办法。因为广度优先搜索的层次遍历解答树的特点,一旦搜索到一个目标节点,那么这时的深度一定是最优解,而不必 象深度优先搜索那样继续搜索目标节点,最后比较才能得出最优解。该搜索方法在这道题目上会遇见致命的问题:广度深度优先搜索是一种盲目的搜索,深度比较大 的测试数据会产生大量的无用的节点,同时消耗很多时间在重复节点的判断上。 为了减少重复的节点,加入人工智能性,马上可以想到用A*算法。经过分析发现,该方法对避免产生大量的无用的节点起到了一定的效果,但是会花97%以上的 时间去判断新产生节点是否与已扩展的和待扩展的节点重复。看来如何提高判重的速度成为该题目的关键。解决这个问题有很多办法,比如引入哈希表,对已扩展的 和待扩展的节点采用哈希表存储,减少判重的代价,或者对已扩展的和待扩展的节点采用桶排序,也可以减少判重的代价。我们现在来尝试一下用 IDA*算法。该算法有个值得注意的地方:对估计函数的选取。如果选用当前状态每个位置上与目标状态每个位置上相同节点的数目加当前状态的深度作为估计函 数,由于当前状态每个位置上与目标状态每个位置上相同节点的数目这个值一般较小,不能明显显示各个状态之间的差别,运行过程中会产生大量的无用的节点,同 样会使效率很低,不能在60s以内完成计算。比较优化的一个办法是选用由于当前状态每个位置上的数字偏离目标节点该数字的位置的距离加当前状态的深度作为 估计函数。这个估计函数的选取没有统一的标准,找到合适的该函数并不容易,但是可以大致按照这个原则:在一定范围内加大各个状态启发函数值的差别。 实践证明,该方法用广泛的通用性,在很多情况下可以替换一般的深度优先搜索和广度优先搜索。 第三部分 搜索算法的优化(二) 该部分将谈到搜索与其他算法的结合。再看scu online judge的一道题目: 给定一个8 * 8的国际象棋棋盘。给出棋盘上任意两个位置的坐标,问马最少几步可以从一个位置跳到另外一个位置。 该题目同样是求最优解,如果用一般的深度优先搜索是很容易超时的。如果用广度优先搜索,会消耗大量的内存,而且效率是很低的。这里,我们将尝试用深度优先搜索加动态规划的算法解决该问题。 将该棋盘做为存储状态的矩阵。每个矩阵元素的值是该位置到初始位置最少需要的步数。初始位置的元素值为0。其他位置的元素初始化为一个很大的正整数。首先 从初始位置开始深度优先搜索,例如某次从(i1,j1)到达位置(i2,j2),如果(i2,j2)处的值大于(i1,j1)的值加1,则(i2,j2) 处的值更新为(i1,j1)+ 1,表示从(i1,j1) 跳到(i2,j2)比从其他地方跳到(i2,j2)更优,不断的进行这个过程,直到不能进行下去位置,那么最后的目标位置的值就是解。这就是一个动态规划 的思想,每个位置的最优解都是由其他能够一次跳到这个位置的位置的值决定的,而且是它们中的最小值。同时,该动态规划又借助深度优先搜索这个工具,完成对 每个位置的值的刷新,可以算是一个比较经典的深度优先搜索和动态规划的结合。该问题还需要注意一个剪枝的问题,从起始位置到目标位置的最大步数是多少?经 过计算,最大值是6。所以一旦某个位置的值是6了,就不必再将它去刷新另外的位置,从而剪去了对很多不必要子树的搜索,大大提高了效率。 第四部分结语 本文的主要的篇幅讲的都是理论,但是根本的目的还是指导实践。搜索,据我认为,是当今ACM竞赛中最常规、也最能体现解题者水平的一类解题方法。 “纸上得来终觉浅,绝知此事要躬行。”要想真正领悟、理解各种搜索的思想,掌握搜索的解题技巧,还需要在实践中不断地挖掘、探索。实践得多了,也就能体会 到渐入佳境之妙了。算法的优化是无穷尽的。