广度优先搜索 VS 水桶问题

Posted 江川编程课

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了广度优先搜索 VS 水桶问题相关的知识,希望对你有一定的参考价值。


我回来了

消失了好长一段,其实就半个月!


经过漫长的努力,我的小可爱终于变成我的小媳妇了,欢迎各位老板祝福!


踏入人生新征程,继续努力学习!


广度优先搜索 VS 水桶问题

姿势准备好,学!


推荐书籍


广度优先搜索 VS 水桶问题

这是我从大量算法书籍中精心筛选出来的,它

  • 重视算法原理的理解,

  • 用生动的例子替代晦涩的公式证明,

  • 绘图也非常有趣,

对非专业开发者十分友好。我的算法教程,也是以《算法图解》 为蓝本展开。

获取渠道

除了购买纸质图书外,你还可以从微信读书搜索到,领用无限读书券非常划算。

准备开始

本节是对《算法图解》第六章的拓展,算法图解对广度优先搜索的解释非常清晰,我几乎无处插嘴。偶然看到知乎老钱提到字节跳动的面试题,顺手解决一下。

水壶问题

这个问题是这样的:

给你一个装满水的 8 升满壶和两个分别是 5 升、3 升的空壶,请想个优雅的办法,使得其中一个水壶恰好装 4 升水,每一步的操作只能是倒空或倒满。


广度优先搜索 VS 水桶问题


 (图非常简单,直接引用了老钱的图,感谢)



初始条件是一个满 8L 的水壶,还有两个空的 5L 和 3L 的水壶。这样的问题在小学的时候没少算,我们需要做的就是:

  • 先这样

  • 再这样

  • 最后这样


凭借聪明的大脑,很容易就能 “凑出” 答案。为什么说是 “凑出” 呢?因为这个问题有更科学、优雅的解决方案:广度优先遍历。

一图足矣

如果我们讲这个问题转换为数学模型,用 (8,0,0) 这样的元组表示 3 个水桶内的水量。其实这道题的答案就藏在这样的图里,说藏可能都有点过分了,因为太明显。


广度优先搜索 VS 水桶问题


我已经将合理的节点用橙色标出,从它就可以确认一条路径,你找到答案了。

图中每一个子节点都是对上一层级的状态,做一次操作更新得到的:

  • 从一个壶里向另一个壶注水

  • 直到这个壶的水注完,或者另一个壶注满。

这张图相当于从(8,0,0) 开始,推演其后所有可能状态及其路径,是不是有点像《算法图解》里提到的路径规划问题,这里是寻找从(8,0,0)到下面任意一个状态的转换路径:

  • (4, *, *)

  • (*, 4, *)

  • (*, *, 4)

(星号表示不关心这里的数据)

有朋友就会很好奇,明明 (5,3,0) 的下一个转换可能是(5,0,3),为什么图里没有画呢?确实,但是 (5,0,3) 已经在之前出现过了,如果这样做了,就会出现“回头路”,形成循环,永远也不会找到答案了。

或者说,这样做是为了使得问题 “收敛”,不至于无限制发散。

计算新状态的时候,丢弃已经出现过的状态称之为“剪枝”。

广度优先和深度优先

以简单一些的二叉树来说明。



广度优先遍历:ABCDEFG... 广度优先遍历需要按照层次遍历,遍历完一层所有节点才继续深入下一层。

深度优先遍历:ABDEICF... 深度优先会直接遍历,探到树的末梢。

对于水桶问题的路径,我们需要找到尽量快的方法得到 4L 水,对于一个路径,每深入一层就是一个步骤。另一方面,出于剪枝的要求,先出现的路径会占据优势,后出现的路径会被淘汰。因此我们需要选用广度优先的遍历方法,在同一层内尽可能寻找答案,不得已的时候,再下探一层,这样可以得到尽可能短的路径。

水桶的实现

这里的水桶本身有约束条件:

  • 只能倒空

  • 或者倒满

因此我实现了一个水杯类,使用 fillup 或者 fill 方法向水桶注入水,以便作为初始条件。

一个特别注意的点,水杯类也允许从一个桶里向另一个桶倒水,只要 fill 的参数是水源的桶就行。

剪枝

剪枝保证了结果的收敛,每当推演出一个状态,就需要检查这个状态是否已经出现过,如果出现过就需要丢弃。这里使用 python 自带的 set —— 集合。集合是无序元素的组合,可以快速的增删成员、判断成员,用在这里正合适。

初始化状态

  • 在 cup_map 中保存了每一步所有可能的出水和入水桶索引。

  • cups 中初始化了 3 个容量分别为 8、5、3 的桶。

  • result_set 记录已经出现过的状态,(8,0,0) 表示最开始第一个桶里有 8L 水,以后当然也不能出现这种情况了。

代码在这里:

广度遍历

为了一层一层遍历路径,这里使用 End_point 表示路径中的一个节点,每个节点记录了从初始状态 (8,0,0) 到当前状态的路径,以及该节点所有水桶的状态。多个 End_point 构成的数组,表示一个遍历层。当遍历到某一层没有数据了,就可以结束遍历了。

注意这里使用了递归:

  • 如果当前层没有节点,退出。

  • 遍历当前层

  • 再遍历下一层

最后的结果

$ python water_cup.py
cup size:
8
5
3
('ok path:', [(8, 0, 0), (3, 5, 0), (3, 2, 3), (6, 2, 0), (6, 0, 2), (1, 5, 2), (1, 4, 3)])
('ok path:', [(8, 0, 0), (5, 0, 3), (5, 3, 0), (2, 3, 3), (2, 5, 1), (7, 0, 1), (7, 1, 0), (4, 1, 3)])

看来最开始我绘制的路线图不完整,有两条合理的结果。这也许就是算法和计算机超越人脑的地方吧。

完整的代码在这里 https://github.com/JiangChuanGo/examples/blob/master/water_cup.py


以上是关于广度优先搜索 VS 水桶问题的主要内容,如果未能解决你的问题,请参考以下文章

算法浅谈——走迷宫问题与广度优先搜索

基础扩展 | 16. 队列应用示例:广度优先搜索

广度优先搜索算法(go)

算法之广度优先搜索

基本算法——深度优先搜索(DFS)和广度优先搜索(BFS)

Prolog中的广度优先搜索