浅谈 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}\) 大家应该都明白了吧!一切安好。

posted @ 2022-11-04 23:40  bifanwen  阅读(281)  评论(0编辑  收藏  举报