3.8 搜索题的一些调试技巧
如果决定用 DFS 做一道题,那么:
如果能套用模板或经典的 DFS 模型,就直接套用。
如果不能套用,就先写爆搜,并命名为#0 版本。
对于每一个版本,调试正确之后,再进行剪枝,并且每次只加一个剪枝,调试正确后再继续。
每次加剪枝都应该开一个新的版本,不能直接修改原版本。
如果时间来不及了,就按照从强到弱或从好写到难写的顺序来加剪枝。
在动态查错之前应该进行一次静态查错。
调试时,如果出现 RE(运行时错误)或死循环,就在出错的状态处停止,并检查其所有的祖先 状态(显然这需要记下每次的决策),看看这些决策是否合法。
如果程序正常结束,但结果错误,应该从以下三个角度入手:
1. 明明有解,输出无解:可以把正解的各个决策一步一步代入,看看程序里面是在哪一步出了 问题(明明合法的决策它木有作出),从而方便检查;
2. 输出不合法的解:可以把形成这个不合法解的决策一步一步代入,看看是哪一步出了问题;
3. 输出合法但非最优的解:必然是某些最优性剪枝错误或者最优性判断出了问题,可以直接到 这些地方去检查,实在不行也可以把最优解代入检查。
有时也可以用分段检查的方法,即把代码中的各个过程或者片段截取下来,将几组小的输入代入,看
看执行之后,所有在该片段里改变了值的全局变量是否正确,进而发现这里的错误。
千万不要一下子输出全部的状态!这样不仅不容易查出结果,还把容易把自己绕晕。
事实上,只有在状态总数不太多,且相对关系明了、顺序整齐的时候(比如一般的 DP 等),可以一下子输出全部状态来检查,否则就不能这么做。
对于 BFS,其实它比 DFS 容易调试,因为所有的状态都储存在队列里。因此,可以在状态结点中记下每个
点的前趋,然后,哪里出了问题,就直接把它的前趋状态全部输出。
另外,BFS 一般没有“剪枝”这一说(除了判重),且决策过程一般是比较整齐的,因此更容易调试。
3.9 小结
搜索,就要用搜索算法来产生所有可能的答案,并从中筛选出正确的答案。
搜索法常用于求可行解或可行解的个数。在竞赛中,搜索法还是“救命稻草”——大多数问题都可以使用
搜索来谋取部分分数。
搜索的方式有很多种,常见的有:枚举搜索、深度优先搜索、广度优先搜索、迭代加深搜索。
解搜索题时要从以下几方面入手:确定状态、确定结点扩展方式、选用合适的搜索方式、优化。
1. 深度优先搜索和广度优先搜索
将搜索时产生的状态组成一棵树,就成了解答树。深度优先搜索和广度优先搜索的区别在于解答树的遍历
方式。下图是一棵解答树:
深度优先搜索用逐步加层的方法搜索并且适当时回溯,遍历过程如下图所示:
广度优先搜索得名于它的实现方式:每次都先将搜索树某一层的所有结点全部访问完毕后再访问下一层, 再利用先前的那颗搜索树,广度优先搜索以如下顺序遍历:
首先访问根结点,而后是搜索树第一层的所有结点,之后第二层、第三层……以此类推。
此外,在使用广度优先搜索时,应小心处理空间问题。
下面是两种搜索算法的比较:
DFS | BFS | |
优势 | 1. 比较适合回溯类搜索 2. 参数传递、状态修改和恢复都比较方便 3. 自顶向下地处理问题 4. 记忆化搜索容易实现 5. 能很快到达解答树的底端 | 1. 解决“最少步数”、“深度最小”问题 2. 问题的解靠近解答树的根结点 3. 启发式搜索在BFS中更容易实现 4. 能立刻停止搜索 |
缺点 | 1. 使用递归算法容易导致栈溢出 2. 有时不容易输出方案 3. 不易立即结束搜索 | 1. 空间一般比DFS大 2. 状态重复的排除有时耗时多 |
2. 迭代加深搜索
广度优先搜索可以用迭代加深搜索代替。迭代加深搜索实质是限定下界的深度优先搜索,即首先允许深度优先搜索搜索 k 层搜索树,若没有发现可行解,再将 k+1 代入后再进行一次以上步骤,直到搜索到可行解。这个“模仿广度优先搜索”搜索法比起广搜是牺牲了时间,但节约了空间。
在需要做 BFS,但没有足够的空间,时间却很充裕的情况下,可以考虑迭代加深搜索。
迭代加深搜索的时间不会超过等效 BFS 占用时间的两倍,当数据规模增大时,二者的差距还会逐渐减小,而迭代加深搜索的空间占用和 DFS 一样小。
3. 搜索的优化
没有经过优化的搜索叫“爆搜”或“裸搜”。爆搜能承受的规模不大,原因有二:一是搜索时产生了无用的结点,二是重复计算。
优化时从以下几方面入手:确定更合适的搜索顺序、剪枝(可行性剪枝、最优化剪枝)、降低考察和扩展结点的代价,必要时可采用高级的搜索方法,或使用状态压缩。
此外,写搜索程序时应该先从朴素算法入手,逐步优化,以降低编程复杂度、减少不必要的失误。