理解广度优先搜索

前段时间遇到了一些与BFS有关的有趣的问题,在一些朋友或者资料的帮助下有所思考,发现这个简单的算法如果能应用自如,的确能发挥强大的功效,于是乎写篇博客记录一下。

BFS概念很简单,此处有介绍;BFS实现也很简单,用一个queue就可以了;而它确实也是图中一个非常重要的算法,而它确实也可以用来解决一些看似与图没啥明显关系的问题。 理解广度优先搜索,关键在于理解其应用。

1. 基本应用

广度优先搜索算法是基于图定义的,所以最直观的应用自然就在图中:有明确的vertex和edge。

比如地图,某个地点就是vertex,而连接两个地点的路径,就是edge,假设edge长度都一样(当然,事实上这是不可能的),我们就能用广度优先搜索求得两点间的最短距离;
比如人际关系图,一个人就是vertex,而两人是否认识(connected) 就是edge,我们熟知的开心网,LinkedIn就是这种情况,此时我们应该可以用广度优先搜索得到两个人之间最少可以通过几个朋友取得联系。 据说我们可以通过6个人介绍从而认识国家主席 - 不算太坏。

然而,有很多问题并不是这么直观的,你可能看不到一个图,看不到BFS的适用性。此时,我们需要足够的洞察力,将问题抽象到一个图上,并选择正确的目标函数来应用BFS。下面几个例子分别予以说明。

2. 迷宫问题

迷宫是由一个一个的方格子组成,有些格子之间是相通的,而有些不是,把一只小白鼠放入这样一个迷宫,问小白鼠怎么样才能最快的走出迷宫

这里迷宫可以用一个简单的二维数组来表示,数组元素值1表示连通,0表示不连通。 这虽然不是一个直接的图的问题,但其实也不难看出其和图的相关性:

vertex:所有值为1的格子。 (值为0的格子因为不连通,从逻辑上来讲并不属于这个图,但我们依赖与它来判断两点间是否有edge存在)
edge: 任意两个相邻的,且值都为1的vertex之间的联系

可以发现,迷宫的这种对于图的表达方式,与传统的邻接表,邻接矩阵表达方式很不相同 (更加紧凑),但是却非常适合来表示迷宫这样的结构。这也是一个图,只不过有其特定的访问方式而已。 指定起点,与终点(任何边界vertex), 在这个数组上应用BFS,就能找出走出迷宫的最短路径,当然,需要注意的是不能重复访问已经访问过的结点,我们可以直接修改表示迷宫的数组,如访问过了就设为0,也可以提供一个辅助数组来标记是否访问过。

这里有篇文章详细的介绍了迷宫问题的解法。

3. 倒酒问题

一个8L杯子装满了酒,有一个一个3L空杯子和一个5L空杯子,问怎样才能用最少的次数倒出4L的酒。(不能倒掉)

这是一个面试中常见的逻辑题,做到这样的题,如果不了解其背后的套路,只是像个没头苍蝇一样不断的去尝试,倒来倒去可能会花很多时间,而且不一定是最少的,甚至,还有可能把自己绕晕。 

一个解法是把每次倒完后3个杯子中酒的数量视为一个状态,而”倒“的过程则是一个状态迁移的过程,初始状态为800, 状态迁移则是把杯子A的酒倒入杯子B,结果要求要么A为空,要么B为满。 我们可以有如下推理过程:

对于这个推理过程,要注意两点:

  • 一是我采用了“宽度优先” 的状态迁移过程,也就是说我的思路过程是从左到右,然后在每一列上从上到下,这可以保证我找到的是最少的步数
  • 二是对于已经出现过的状态,我不再处理,不然问题规模不会逐渐缩小(如从800到530后,很自然在扩展530的时候,我还可以倒回去成为800,但因为800出现过,不再处理)

这里,800-305-332-602-620-125-134这条路线以一步的优势胜出!

可以看出,这里其实就是应用了广度优先搜索算法,对于更加复杂的问题(如5个杯子啥的),完成可以编程予以解决。 那么图在哪里?
vertex:某个时刻三个杯子中酒的状态 
edge:可以通过一次倒酒实现的从一个状态到另外一个状态的”迁移“。

为了标志某个状态是否访问过,我们可以用一个大小为1000的数组来表示,默认值为0,某时刻的状态数值为数组下标,也即100*a + 10*b + c。(思考,如果有杯子容量大于10呢?)

(感谢atyuwen同学给出了这个思路)

有了这样的思路,对于解决下面这个过桥问题应该比较容易了:

四个女人过桥,夜间有一火把,每次最多过两个,必需带火把,过桥速度不一样 1min, 2min, 5min, 10min; 两个人过用最慢一个的速度,火把不能扔,如何在最快的时间内让四个女人都过桥?

这个问题可以通过思考来解决,原则就是尽量让快的人在一起,慢的人在一起,避免快的被慢的人拖累(这和多线程的负载平衡类似),这样解法就是:

  • 1, 2过去1回来:3
  • 5,10过去2回来:12
  • 1,2过去:2 

17分钟,但是如果人比较多的话,你可能也无法确定是不是最快了。 这个问题其实也可以用图的BFS算法解决:
vertex: 在对面的人的状态,可以用一个4位的二进制数表示,没一为分别对应一个人的状态,0000表示一个都没过去,1111表示都过去了
edge:两个人走过去,如果还没全过去的话,再一个人回来,从而发生的状态“迁移”

这样,算法应该不难出来了。

4. 连连看

这其实是《编程之美》第一章”1.14 连连看游戏设计“中介绍的一个算法,连连看可以由一个二维数组表示,数组元素的值表示不同的图形,我们可以用0表示该位置没有图形。 连连看中两个结点能够相连从而消去的标准是:相连不超过两个弯的相同图形:
 

(图片取自编程之美)

这个问题的结构与迷宫问题有点类似,可是为了应用BFS,我们的大脑也得转个弯,玩家依次点了两个结点后,我们需要判断是否可以消去:

  • 图形是否相等非常简单,只要判断该处元素的值即可
  • 相连是否不超过两个弯,我们需要从中取一个结点为起始点,先拿到无须转弯就能到达的结点集合A,看目标结点是否在里面;如果不在,则需要对结点集合A中所有结点,拿到其无须转弯就能到达的结点(未在之前访问过),判断目标结点是否在内;如果不在,则继续扩展,这次如果再不在,说明两结点连接超过两个弯了,不满足

此处, vertex是所有没有图形的结点,而edge则是任意两个在同一直线上的,未被有图形的结点截断的结点之间的联系。BFS的应用在于结点距离不超过2次,此处距离是指转弯次数。

通过上诉例子,BFS之强大与有趣可见一斑!

posted @ 2011-04-17 21:29  lzprgmr  阅读(15379)  评论(4编辑  收藏  举报

黄将军