广度优先搜索
广度优先搜索(BFS),可以被形象的描述为“浅尝辄止”,具体一点就是每个顶点只访问它的邻接节点(如果它的邻接节点没有被访问)并且记录这个邻接节点,当访问完它的邻接节点之后就结束这个顶点的访问。
广度优先用到了“先进先出”队列,通过这个队列来存储第一次发现的节点,以便下一次的处理;而对于再次发现的节点,我们不予理会——不放入队列,因为再次发现的节点:
- 无非是已经处理完的了;
- 或者是存储在队列中尚未处理的。
《算法导轮》对两种搜索都采用了很聪明的做法,用白色WHITE来标志未发现的节点,用灰色GRAY来标志第一次被发现的节点,用黑色BLACK来标志第二次被发现的节点。
于是有了:
BFS(G,s)
for
each vertex v in V[G]
status[v] = WHITE
/******其他初始化******/
status[s] = GRAY
//s是原点
queue q
入队(q,s);
while
q非空
t = 出队(q);
for
each vertex v in Adj[t]
//与t邻接的点
if
status[v] = WHITE
//只对未访问的操作
status[v] = GRAY
//标记为第一次访问
/******其他操作******/
入队(q,v)
status[t] = BLACK
//此点已经处理完了
|
关于广度优先搜索的一个简单应用:
假如有问题,每个村庄之间都通过桥来联通,先给出村庄的图,问村庄A到村庄B最少要通过多少座桥?这个问题可以很容易的转化为上面的BFS问题。
例子1:倒酒问题
一个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:可以通过一次倒酒实现的从一个状态到另外一个状态的”迁移“。
四个女人过桥,夜间有一火把,每次最多过两个,必需带火把,过桥速度不一样 1min, 2min, 5min, 10min; 两个人过用最慢一个的速度,火把不能扔,如何在最快的时间内让四个女人都过桥?
这个问题可以通过思考来解决,原则就是尽量让快的人在一起,慢的人在一起,避免快的被慢的人拖累(这和多线程的负载平衡类似),这样解法就是:
- 1, 2过去1回来:3
- 5,10过去2回来:12
- 1,2过去:2
17分钟,但是如果人比较多的话,你可能也无法确定是不是最快了。 这个问题其实也可以用图的BFS算法解决:
vertex: 在对面的人的状态,可以用一个4位的二进制数表示,没一为分别对应一个人的状态,0000表示一个都没过去,1111表示都过去了
edge:两个人走过去,如果还没全过去的话,再一个人回来,从而发生的状态“迁移”
这样,算法应该不难出来了。
或者画出所有可能的状态图,然后用dijkstra算法求出源状态到目的状态的最短路径。参照http://wenku.baidu.com/link?url=emD2KFB7HVMiYDqo5uwoO8IP-u-lpujgsHZrhQKoIK6DErCZdoh12ZfCtTCsZ0b1axA1eEoras42CEeyovEKM8XhGQj7TemIp1flBbyQS6a
根
根
深度优先搜索
深度优先搜索(DFS),可以被形象的描述为“打破沙锅问到底”,具体一点就是访问一个顶点之后,我继而访问它的下一个邻接的顶点,如此往复,直到当前顶点一被访问或者它不存在邻接的顶点。
同样,算法导论采用了“聪明的做法”,用三种颜色来标记三种状态。但这三种状态不同于广度优先搜索:
- WHITE 未访问顶点
- GRAY 一条深度搜索路径上的顶点,即被发现时
- BLACK 此顶点的邻接顶点被全部访问完之后——结束访问次顶点
DFS(G,s)
for
each vertex v in V(G)
status[v] = WHITE
/******其他初始化******/
for
each vertex v in V(G)
if
(status[v]==WHITE)
DFS-VISIT(v)
DFS-VISIT(v)
status[v] = GRAY
for
each vertex t in Adj(v)
if
status[t] = WHITE
DFS-VISIT(t)
/******其他操作******/
status[v] = BLACK
|
通过给DFS搜索过程中给每一个顶点加时间戳,就可以实现拓扑排序了。实现拓扑排序需要:
对于每一个顶点,都有两个时间戳,分别这样来定义:
- 在一顶点刚被发现的时候,标记此顶点的第一个时间戳;
- 在结束此顶点的访问的时候,标记此顶点的第二个时间戳。时间戳可以用简单的123456来标记,只要能区分大小就行。
总结
两个算法都是O(V+E),在用到的时候适当选取。在使用白灰黑标志的时候,突然明白了如何用深度优先搜索来判断有向图中是否存在环。
深度优先和广度优先各有各的优缺点:
- 广优的话,占内存多,能找到最优解,必须遍历所有分枝. 广优的一个应用就是迪科斯彻单元最短路径算法.
- 深优的话,占内存少,能找到最优解(一定条件下),但能很快找到接近解(优点),可能不必遍历所有分枝(也就是速度快), 深优的一个应用就是连连看游戏.
在更多的情况下,深优是比较好的方案。