浅谈 BFS 与 IDDFS
前记
为什么没有 \(\text{CSDN}\) 和博客园的同步输出了呢?因为博主懒得贴实践代码(实际上 \(6\) 种搜索的效率对比数据本人是有的),认为 \(\text{CSDN}\) 和博客园发博文不会得到什么欢迎,就在洛谷发一发吧。
引入
很多朋友都会在学 \(\text{IDDFS}\)(迭代加深搜索) 的时候问我:
- \(\text{IDDFS}\) 和 \(\text{BFS}\) 有啥区别?
诚然,它们有一定的相似,不可否认。
本质搜索方式
下面我们来剖析算法的原理。
\(\text{BFS}\) 的原理是把每一层逐层地往下搜。
而 \(\text{IDDFS}\) 也是这样,但它 规定搜的层数。
那你会说了:\(\text{IDDFS}\) 可能会重复搜一些状态啊!比方说,它限定层数为 \(d\),从 \(1\) 搜到 \(d\) 会把第 \(1\) 层的状态来回的搜索。这样反而不如 \(\text{BFS}\) 单一地搜啊!
你说的很有道理,你掌握了 \(\text{IDDFS}\) 的精髓,可矛盾的是,为什么它总会比 \(\text{BFS}\) 快呢?
那是因为它还有一个黑科技!
回归各种搜索算法
在讲这个黑科技之前,我们按照一定的顺序捋一捋搜索的算法。
-
深度优先搜索
-
宽度优先搜索
-
双向宽度优先搜索
-
A*
-
IDA*
-
迭代加深搜索
前两种非常简单,无可厚非;而第三种 只是在第二种搜索的基础上进行双向搜索 而已。
那 \(\text{A*}\) 是啥呢?显然,它引入了一个 估价函数 的概念!即,对当前的状态进行一个估价,如果不如之前的状态就不搜了!
这是非常好的一个剪枝,但如果 估价函数 写错,整个程序可能都错了。\(\text{A*}\) 是建立在 深度优先搜索 基础之上的。
而 \(\text{IDA*}\) 同样用来估价函数,它是建立在 宽度优先搜索 上的。
迭代加深搜索就是今天的 \(\text{IDDFS}\) 了,它会 依次规定层数,并往下进行搜索,是建立在 深度优先搜索 和 \(\text{A*}\) 之上的。
实际上,上面的六种搜索已经按照时间效率从低到高排好序。\(\text{BFS}\) 与 \(\text{IDDFS}\) 的效率为何相差这么大呢?同样都是按层搜索啊!
理论剖析
首先,我们知道 \(\text{BFS} = \text{Breadth-First Search}\),那么 \(\text{IDDFS} = \text{Iterative Deepening Depth-First Search}\),这个单词翻译过来的意思就是 迭代深化深度优先搜索,很长。
它以 \(\text{DFS}\) 为基础,所以是 深度优先搜索,那么“迭代深化”是什么意思呢?通过百度百科可以得到:
迭代是重复反馈过程的活动,其目的通常是为了逼近所需目标或结果。
每一次对过程的重复称为一次“迭代”,而每一次迭代得到的结果会作为下一次迭代的初始值。
这说明什么呢?
说明迭代与 \(\text{DFS}\) 是很相似的过程。我们可以认为 \(\text{IDDFS}\) 将 \(\text{DFS}\) 的“迭代过程” 与 \(\text{BFS}\) 的“加深过程”进行了综合。
原理上,\(\text{DFS}\) 会把以 \(u\) 为根的 搜索树的状态全部搜完,才会进行对 \(u\) 的回溯。而 \(\text{BFS}\) 会把以 \(u\) 为根拓展的节点搜出来,一层层的进行状态搜索。实际上又回归到了树的两种核心遍历:\(\text{DFS}\) 和 \(\text{BFS}\).
如何把这两种算法进行糅合?我们考虑按照 \(\text{BFS}\) 的方式,规定当前搜的层数 \(d\),从小到大搜。对于每个 \(d\) 如何验证答案?此时要用 \(\text{DFS}\) 把 层数 \(d\) 以内的搜索树的状态搜完,有解则返回,无解则 \(d \gets d + 1\),继续搜。
但是你会发现,验证答案的过程同样可以换成 \(\text{BFS}\),那样“迭代加深”就失去了它的意义。此时经典的一步来了。我们考虑一个“估价函数”,因为有了 \(d\) 的限制,很多状态会被剪枝。
具体的,假设当前状态用了 \(g(x)\) 步,估价 \(h\) 认为 到达目标状态至少需要 \(h(x)\) 步,总共的搜索步数不能超过 \(f(x)\) 步,此时 \(f(x) = d\),那么应满足 \(g(x) + h(x) \leq f(x)\),否则我们可以剪枝。
这样的话,\(\text{BFS}\) 会把逐层状态搜完,而 \(\text{IDDFS}\) 有了估价函数的黑科技,就可以只搜部分的状态,并且对于当前状态有效的剪枝。
简单比对
那么你会问了,\(\text{IDDFS}\) 的验证过程可不可以换成 \(\text{BFS}\) 和估价函数的实现呢?实际上是可以的。但是那样的话,\(\text{IDDFS}\) 和 \(\text{IDA*}\) 将会非常相似,失去了“加深”的意义。实际上“迭代”是 \(\text{BFS}\) 的思想,“加深”是 \(\text{DFS}\) 的思想。这样可以使得 \(\text{IDDFS}\) 在搜索算法中效率到达顶峰。
简单的说吧,就 \(4 \times 4\) 的数独搜索题,前 \(4\) 种搜索都 很难通过,只有用 \(\text{IDA*}\) 和 \(\text{IDDFS}\) 才可以。实际上搜索的优化不是一戳而就的。
搜索历史
首先有了最简单的 \(\text{DFS}\),然后人们逐渐发现有些题目层数是较小的,但是深度很大,加上树的搜索基础,\(\text{BFS}\) 就应运而生。
接着人们发现 \(\text{BFS}\) 也不够快,于是考虑一个“起点和终点同时出发”的算法,并把 \(\text{BFS}\) 的时间复杂度降到了 \(\sqrt{}\) 级别,使得双向搜索被广泛应用。实际上 双向搜索在背包问题中对 \(n \leq 40\),\(v_i , w_i \leq 10^9\) 也有应用。
人们在对双向搜索的调试过程中,发现有的时候 程序会明显向更劣的状态进行搜索,于是 估价函数 应运而生,这样出现了 \(\text{A*}\),与双向搜索的效率不相上下,但 \(\text{A*}\) 总体效率较高。
再后,人们又发现在 \(\text{A*}\) 的过程中可以规定深度,这样有了 \(\text{IDA*}\),通过实践和理论证明也证明了 \(\text{IDA*}\) 比 \(\text{A*}\) 要优。
最后,人们把 \(\text{IDA*}\) ,\(\text{DFS}\) 与 \(\text{BFS}\) 结合,出现了 \(\text{IDDFS}\),把搜索的效率推到了顶峰。在数独方面,\(\text{IDDFS}\) 的搜索效率极高。(实际上 \(\text{Dancing Links}\) 的效率应当更高些,它的时间复杂度是稳的)
后记
因此 \(\text{IDDFS}\) 与 \(\text{BFS}\) 大家应该都明白了吧!一切安好。