广度优先搜索 VS 水桶问题
Posted 江川编程课
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了广度优先搜索 VS 水桶问题相关的知识,希望对你有一定的参考价值。
我回来了
消失了好长一段,其实就半个月!
经过漫长的努力,我的小可爱终于变成我的小媳妇了,欢迎各位老板祝福!
踏入人生新征程,继续努力学习!
姿势准备好,学!
推荐书籍
这是我从大量算法书籍中精心筛选出来的,它
重视算法原理的理解,
用生动的例子替代晦涩的公式证明,
绘图也非常有趣,
对非专业开发者十分友好。我的算法教程,也是以《算法图解》 为蓝本展开。
获取渠道
除了购买纸质图书外,你还可以从微信读书搜索到,领用无限读书券非常划算。
准备开始
本节是对《算法图解》第六章的拓展,算法图解对广度优先搜索的解释非常清晰,我几乎无处插嘴。偶然看到知乎老钱提到字节跳动的面试题,顺手解决一下。
水壶问题
这个问题是这样的:
给你一个装满水的 8 升满壶和两个分别是 5 升、3 升的空壶,请想个优雅的办法,使得其中一个水壶恰好装 4 升水,每一步的操作只能是倒空或倒满。
(图非常简单,直接引用了老钱的图,感谢)
初始条件是一个满 8L 的水壶,还有两个空的 5L 和 3L 的水壶。这样的问题在小学的时候没少算,我们需要做的就是:
先这样
再这样
最后这样
凭借聪明的大脑,很容易就能 “凑出” 答案。为什么说是 “凑出” 呢?因为这个问题有更科学、优雅的解决方案:广度优先遍历。
一图足矣
如果我们讲这个问题转换为数学模型,用 (8,0,0) 这样的元组表示 3 个水桶内的水量。其实这道题的答案就藏在这样的图里,说藏可能都有点过分了,因为太明显。
我已经将合理的节点用橙色标出,从它就可以确认一条路径,你找到答案了。
图中每一个子节点都是对上一层级的状态,做一次操作更新得到的:
从一个壶里向另一个壶注水
直到这个壶的水注完,或者另一个壶注满。
这张图相当于从(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 水桶问题的主要内容,如果未能解决你的问题,请参考以下文章