grokking algorithms breadth-first search(BFS) 第六章 广度优先搜索算法 中文翻译

说明:《grokking algorithms》算法讲解的非常深入浅出,用好多幅图以及日常常用的例子讲算法讲解的非常好,若是英文还可以的话,建议看英文原版,非常喜欢作者的写作风格,将复杂的算法用简单的方式描述出来,真的好厉害!!

在本章中
························· ·······················
你会学到一个新的、抽象的描述网络的数据结构:图;
你会学到广度优先搜索算法,你可以运行图来回答一下类似问题“到X的最短路径”;
你会学到有向图和无向图;
你会学到拓扑排序,一种可以展示节点间相互依赖关系的排序算法。
····················································
这一章介绍图,首先介绍什么是图(它们并不包含X轴和Y轴)我会介绍第一个图算法,叫广度优先搜索算法(BFS)。
广度优先搜索算法可以找到两点间的最短距离。但是最短距离可以指代很多事情。你可以利用广度优先搜索来做
写一个检查AI器,怎样移动最少步数来获得胜利。
写一个拼写检查器(错误输入时联想到正确的单词,例如 READED 联想到是READER)
在你附近寻找离你最近的医生

图算法是我知道的非常有用的算法之一。请确保你下面几章要好好学—这些事以后你可能经常使用的。

图的简介

这里写图片描述

图6-1
假设你住在圣弗朗西斯科,你想从双子峰到金门大桥,你想通过公交车并且少换乘,下面是你可以选择的路径。

这里写图片描述
图6-2

你通过什么算法来找到最少的方式呢?
我们可以直达吗?下面是你可以直达的一些地方。(看起来像太阳的点可以直达)

这里写图片描述

图6-3

金门大桥没有被加粗,你不能直达金门大桥。你换乘一路公交可以直达吗?

这里写图片描述

图6-4

同样地,换乘一站也不能。因此,在2步内你不能到达金门大桥,那3步呢?

这里写图片描述
图6-5
啊哈,现在你可以到达了。所以走这个路径。你换成3次可以到达金门大桥。
这里写图片描述

图6-6

也有其他的路径可以到达金门大桥,但是它们更长,需要换乘4次,这算法告诉我们最短的路径是换乘3次。这种问题被称为最短路径问题。你经常在找最小的事物。如去你朋友家最短的路径,在国际象棋中走最少的步数将对方“将死”。解决最短路径的算法被称为广度优先搜索算法。
找到从双子峰到金门大桥的最短路径,需要两步:
将问题抽象成图
利用广度优先搜索算法来解决问题。
现在,我会介绍什么是图。然后详细讲述广度优先搜索算法。

什么是图?

这里写图片描述
图6-7

图是连接的集合,假设你和你的朋友们在玩扑克。你需要标示谁欠谁钱。这样你可以说,“Alex欠Rama钱”
ALEX——–> RAMA
现在完整的图大概是这样子的。

这里写图片描述
图6-8
Alex欠Rama钱,Tom欠Adit钱等等。每张图都是由节点和边组成的。

这里写图片描述
图6-9

这就对了。图是由边和节点组成的,一个节点可以和多个其他节点连接,其他和这个极点有连接的节点是它的邻居。在这张图中,Rama是Alex的邻居,Adit不是Alex的邻居,因为它们不直接相连。但是Adit是Rama和Tom的邻居。
图是用来标示不同事物是如何相互连接的。让我们来练习下广度优先搜索算法。

广度优先搜索

我们在第一章中学习了查找:二分查找,广度优先搜索是另一种类型的查找算法:一种可以作用于图的算法。它可以回答以下两类问题:
第一类问题:是不是有从A到B的路线?
第二类问题:从A到B最近的路径是哪个?

你已经使用过一次广度优先搜索了,当你计算从双子峰到金门大桥最短的路径时,这是第2类问题—“什么是最短路径”,“让我们来更详细地学习算法”,你可能会问第一个问题:“这儿有路径吗?”

这里写图片描述
图6-10

假设你是光荣的芒果农场主,你需要找经销商来销售你的芒果,你能从Facebook找到吗?但是,你可以问问你的朋友们。
这里写图片描述

图6-11

这个查找是非常直接的。
第一,将你能询问的朋友放在列表中。

这里写图片描述
图6-12
第二,去找到表中的每一个人,然后询问他们是否卖芒果。

这里写图片描述

图6-13
假设你的朋友们都不卖芒果,那你需要询问你的朋友的朋友们。

这里写图片描述
图6-14

当你询问某个你的朋友的朋友时,将他们加入到列表中。

这里写图片描述
图6-15
这样,你不止询问了你的朋友们,还询问了朋友的朋友们。记住,我们的目的是在你的朋友圈中找到可以帮你销售芒果的人。假设Alex不卖芒果,我们可以将Alex的朋友加进来。这也意味着我们可以找他的朋友们的朋友的朋友的朋友……,用这个算法一直找,知道找到芒果经销商,这就是广度优先搜索算法。

寻找最短路径

回顾一下,广度优先搜索回答了2个问题
第1类问题:是否有从节点A到节点B的路线?(在你的朋友圈中是否有芒果经销商?)
第2类问题:从节点A到节点B的最短路径是什么?(谁是离你最近的芒果经销商?)
你知道怎样回答问题1,我们来试着回答问题2.你能找到最近的芒果经销商吗?比如,你的朋友是第一级连接,你的朋友的朋友是第二级连接。

这里写图片描述
图6-16
比起第二级连接,你更倾向于选择第一级连接,比起第三级连接,你更倾向于选择第二级连接等等。所以在你没有完全确定第一级连接中没有卖芒果的人之前,你不会去查找第二级连接,这就是广度优先搜索做的。

广度优先搜索是这么起作用的。搜索人从起点开始辐射,因此你找完第一级连接后,才会去找第二级连接。突击小测验:Claire和Anuj,谁会被第一个查找? 答案:Claire是第一级连接,Anuj是第二级连接,所以,Claire要在Anuj之前被询问。
这里写图片描述

图6-17
另一种思路是:第一级连接比第二级连接先加入搜索列表,你可以根据这个列表逐个查看他们是否是芒果经销商。先查找第一级连接,然后查找第二级连接,这样,你就能找到离你最近的了。广度优先搜索不止可以找到A点到B点的路径,而且可以找到从A点到B点的最短的路径。

队列

队列和生活中的例子很像。假设你和你的朋友们在公交车站排队。若是你在你的朋友的前面,那么你比他先上公交车。队列也是这么工作的,队列和堆栈类似,你不能将元素随机的插入队列中,相反,在队列中只有两种操作,入列和出列。
这里写图片描述

图6-18

这里写图片描述
图6-19
若是你将两项插入列表,那么先插入的项先出队列,你可以理解为先进先出。最先被加入的人也会先出队列,而且先被搜索。
队列被称为FIFO数据结构:先进先出。堆栈是LIFO数据结构:后进先出。

这里写图片描述
图6-20
现在我们知道了什么是队列,让我们来实施广度优先搜索。

练习

用广度优先搜索算法作用于每一个图中,来找到解决方案。

这里写图片描述
图6-21
6.1 找到从起点到终点的最短路径。

这里写图片描述
图6-22
6.2 找到从”cab”到”bat”最短的路径。

图的代码描述

这里写图片描述
图6-23
首先,我们将图用代码描述。一张图由很多节点组成。每个节点和它的邻居节点相连,你怎么来描述这种连接,比如“你—> Bob”?很幸运,你知道有种数据结构可以描述这种关系:哈希表。
记住,哈希表以键值对的方式存在。在这个例子中,健是节点,它的邻居是值。

这里写图片描述
图6-24
这是Python描述
graph = {}
graph[“you”] = [“alice”, ”bob”, ”claire”]
“你”的值是一个数组,里面存储了你的邻居们。
图是节点和边的连接束,在Python中需要描述节点和边,若是像下面这样的复杂一点的图,要怎样表示呢?

这里写图片描述
图6-25
这是Python描述

graph = {}
graph[“you”] = [“alice”,  “bob”, “claire”]
graph[“bob”] = [“anuj”,  “peggy”]
graph[“alice”] = [“peggy”]
graph[“claire”] = [“thom”, “jonny”]
graph[“anuj”] = []
graph[“peggy”] = []
graph[“thom”] = []
graph[“jonny”] = []

突击小测验:你描述图的顺序会有影响吗?
比如这样写:

graph[“claire”] = [“thom”, “jonny”]
graph[“anuj”] = []

而不是这样写:

graph[“anuj”] = []
graph[“claire”] = [“thom”, “jonny”]

想想上一章的答案:没有影响。哈希表没有顺序。因此你输入的键值对的顺序是没有影响的。
Anuj,Peggy,Thom和Jonny都没有邻居,因此有箭头指向他们,但是没有箭头从他们指向其他人。这被称为有向图—只有一种指向关系。因此Anuj是Bob的邻居,但是Bob不是Anuj的邻居。无向图没有箭头,他们是彼此的邻居。比如,下面这两种描述方式是等价的。

这里写图片描述
图6-26

算法实现

让我们来回忆下,它是怎么实现的
1.将要确认的项放到队列中
2.从队列中拿出一个
3.确认下这个是否是芒果经销商。

这里写图片描述

图6-27
注意

当我们更新队列时,我用了入列(enqueue)和出列(dequeue),你还会遇到Push和Pop。Push和enqueue是一个意思,Pop和dequeue是一个意思

在Python中,从队列开始,双向队列被描述为dequeue。

from collections import deque
search_queue = deque( ). 创建一个队列
search_queue = graph[“you”] 将你所有的邻居加入到搜索队列中
记住,因graph[“you”]中记录了你的邻居列表,像[“alice”, “bob”, “claire”].这些都被加入到查找队列中。

这里写图片描述
图6-28
让我们看剩下的代码

while search_queque:
    person = search_queue.popleft( )
    if person_is_seller(person):
        print person + “ is a mango seller!”
        return True
    else:
        search_queue += graph[person]
return False

最后一个问题,person_is_seller方法告诉我们谁才是芒果经销商。这是方法:

def person_is_seller(name):
    return name[-1] == ‘m’

这个功能来校验哪一个人的名字是以字母m来结尾的。若它含有“m”,那么它是芒果经销商。看起来有点傻,但是在这个例子中,它是起作用的。让我们来看一下。
这里写图片描述

图6-29
等等,这个算法会一直运行,直到
找到芒果经销商
队列是空的,也就是说,没有芒果经销商
Alice和Bob有共同的朋友:Peggy。所以,Peggy被加入队列中两次。当你加入Alice的朋友时,你加入了Peggy。当你加入Bob的朋友时,你还会加入她,这样在队列中就有两个Peggy。

这里写图片描述
图6-30
但是你只需要确认一次Peggy是不是芒果经销商。确认两次是没有必要的。所以查找完一个人后,你需要来记录下以防她被查找两次。
若你不这样做,你可能会陷入死循环。比如,若是芒果经销商图是这样的。

这里写图片描述
图6-31
开始时,这个查找队列含有你的邻居们。

这里写图片描述
图6-32
现在我们查看Peggy,她不是芒果经销商。因此我们将她的邻居们加入到搜索队列。

这里写图片描述
图6-33
然后我们来确认下自己,自己也不是芒果经销商,因此,将你的邻居们加入队列。

这里写图片描述
图6-34
一直进行,这是个死循环。因此它会从You到Peggy到You,一直循环。

这里写图片描述
图6-35
在检查一个人之前,一定要确保他以前没有被检查过。为了这样做,我们需要将已经检查过的人存入一个数组中。
考虑到这一点,这是最终的广度优先搜索的代码。

def search(name):
    search_queue = deque()
    search_queue += graph[name]
    searched = []
    while search_queue:
        person= serch_queue.popleft()
        if not person in serached:
            if person_is_seller(person):
                print person + "is a mango seller!"
                return True
            else:
                search_queue += graph[person]
                searched.append(person)
    return False

search(“you”)

试着自己运行下代码,试着改变下person_is_seller的代码实现方式也是很有意义的,看看和你预期的是否一样。

运行时间

这里写图片描述
图6-36

若是你查了芒果经销商的所有网络,也就意味着你遍历了所有的边(记住,边是从一个人到另一个人的连接,也是带有箭头的),因此,运行时间最少是O(边的数目)。
你有一个包含每个人的队列,加入一个人到队列中需要的时间是O(1)。将所有的人加入需要O(人的数目),广度优先搜索需要的时间是O(人的数目+边的数目),经常被描述为O(V + E). (V是节点的数目,E是边的数目)

练习

这是一个有关起床后各项活动的图

这里写图片描述
图6-37
这张图告诉我们刷牙之前不能吃饭,因此吃早饭依赖于刷牙。
另一方面,淋雨不依赖于刷牙。因为在刷牙前可以先淋浴,从这张图中,我们可以整理出早上必须做的活动清单。
1.起床
2.淋浴
3.刷牙
4.吃早餐
注意,淋浴的位置是可以移动的,下面的这张清单也是有效的。
1.起床
2.刷牙
3.淋浴
4.吃早饭
6.3 下面的3张表中, 标注每一张表是有效的还是无效的

这里写图片描述
图6-38

6.4 这里有一张大一点的图,做一张有效的清单出来

这里写图片描述
图6-39

你可能会说,在某种意义上,这张图是排序后的,若是A依赖于B,那么在列表中先放B,再放A,这被称为拓扑排序。这是将图转换成有序列表的一种方式。假设你在筹办一个婚礼,有很多的需要做的事情组成了一张图—你不知道从哪开始,你可以将这张图拓扑排序。
假设有一个族谱图。

这里写图片描述
图6-40
这是一张图,因为有节点(人)和边,边指向节点的父母,但是所有的边都向下指,因为向上指是没有意义的—-你的父亲不能是你的祖父的父亲。

这里写图片描述
图6-41
这被称为树。树是图的一种特殊方式,是一种单边指向的图。
6.5 下面的图中,哪些是树?

这里写图片描述
图6-42

复习

广度优先搜索告诉我们是否有A点到B点的路径;
若是有,广度优先搜索算法可以找到最近的路径;
若是你有“找最短X”的问题,可以先将问题抽象成图,然后利用广度优先搜索来解决;
有向图带有箭头,箭头指出关系(rama—>adit 表示rama欠adit的钱);
无向图没有箭头,表示相互指向(ross—rachel表示rachel和ross约会,ross和rachel约会);
队列是FIFO的(先进先出);
堆栈是LIFO的(后进先出);
你需要校验这个人是否被加入过搜索列表。因此搜索列表需要一个队列,否则,你得不到最短路径。;
当你检查某人时,你需要确认不重复检查。否则,你会陷入死循环。
这里写图片描述

图6-43

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值