目录
第六章 广度优先搜索
用新的数据结构图来建立网络模型。
学习广度优先搜索。(第一种图算法——广度优先搜索(breadth-first search,BFS)。 )
学习有向图和无向图。
学习拓扑排序,这种排序算法指出了节点之间的依赖关系。
广度优先搜索让你能够找出两样东西之间的最短距离,不仅仅是狭义上的距离,含义很多:
6.1 图简介
假设你要从双子峰去金门大桥,为找出换乘最少的乘车路线,你将使用什么样的算法?
接下来按照下面这个思路分析:
一步能到达金门大桥吗?不能。那标出一步能到达的地方。
两步能到达金门大桥吗?标出两步能到达的地方。
三步能到达金门大桥吗?可以。那么到达金门大桥换乘最少的路线已经找出来,需要三步。其他能到达金门大桥的路线会更远,需要四步。
这种问题被称为最短路径问题(shorterst-path problem)。解决最短路径问题的算法被称为广度优先搜索。
确定如何从双子峰前往金门大桥,需要两个步骤。
(1) 使用图来建立问题模型。
(2) 使用广度优先搜索解决问题。
6.2 图是什么
图用于模拟不同的东西是如何相连的。
绘制一个欠钱图。比如下图表示Alex欠Rama钱。
完整的欠钱图可能类似于下面这样:
图由节点(node)和边(edge)组成。 一个节点可能与众多节点直接相连,这些节点被称为邻居。图用于模拟不同的东西是如何相连的。
6.3 广度优先搜索
广度优先搜索是一种用于图的查找算法,可帮助回答两类问题。
第一类问题:从节点A出发,有前往节点B的路径吗?
第二类问题:从节点A出发,前往节点B的哪条路径最短?
解决第一类问题,有没有路径,例子:
我想看我的朋友有没有人是芒果经销商,我就列一个朋友清单,一个一个找。
如果没有朋友是芒果经销商,那就在朋友的朋友中找。在检查每个朋友时,如果他不是,就把他的朋友也加入到查找清单。
不仅在朋友中查找,还在朋友的朋友中查找,使用这种算法将搜遍你的整个人际关系网,直到找到芒果销售商。这就是广度优先搜索算法。
6.3.1 查找最短路径
解决第二类问题:最短路径,例子:
谁是关系最近的芒果售商。例如,朋友是一度关系,朋友的朋友是二度关系。
一度关系胜过二度关系,二度关系胜过三度关系,以此类推。因此,你应先在一度关系中搜索,确定其中没有芒果销售商后,才在二度关系中搜索。广度优先搜索就是这样做的。
广度优先搜索不仅查找从A到B的路径,而且找到的是最短的路径。
只有按添加顺序查找时,才能实现这样的目的。有一个可实现这种目的的数据结构,那就是队列(queue)。
6.3.2 队列
队列类似于栈,你不能随机地访问队列中的元素。队列只支持两种操作:入队和出队。
先加入的元素将在后加入的元素之前出队。(先加入的元素先出队)
队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。
练习1
对于下面的每个图,使用广度优先搜索算法来找出答案。
6.1 找出从起点到终点的最短路径的长度。
答:最短路径长度为2。
6.2 找出从cab到bat的最短路径的长度。
答:最短路径长度为2。
6.4 实现图
用代码来实现图。
每个节点都与邻近节点相连,如果表示类似于“你→Bob”这样的关系呢?要将节点映射到其所有邻居。,如下图:
表示这种映射关系的Python代码如下:
graph = {}
graph['you'] = ["alice", "bob", "claire"]
graph["you"]是一个数组,其中包含了“你”的所有邻居。
那更大的图怎么表示,用代码表示下面这张图就是:
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"] = []
有向图和无向图
有向图单箭头,关系是单向的。无向图没有箭头,直接相连的节点互为邻居。例如,下面两个图是等价的。
6.5 实现算法
这种算法的工作原理。:
这就是入队出队,也叫作压入弹出。
首先,创建一个队列。在Python中,可使用函数deque来创建一个双端队列。
我们来编写查找芒果商的代码
实现图和广度优先搜索代码
#python 内置函数创建队列
#在Python中,可使用函数deque来创建一个双端队列。
#创建关系图
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"] = []
from collections import deque
def search(name):
search_que = deque()
search_que += graph[name]
searched = [] #用于记录检查过的人
while search_que: #只要search_que不为空
person = search_que.popleft() #取出队列第一个人
if not person in searched:
if person_is_seller(person): #判断是不是芒果商的函数
print(person+" is a seller") #是芒果商
return True
else:
search_que += graph[person] #不是芒果商就把他的所有朋友加入搜索队列
searched.append(person)
return False #如果运行到这里,就说明队列中没有芒果商
def person_is_seller(name):
return name[-1] == 'm' #我们这里假设以m结尾的姓名,是芒果商
search('you')
OUT:
thom is a seller
True
广度优先搜索的运行时间
要在人际关系网中沿每条边搜索芒果商,运行时间至少为O(边数)。
当遇到不是芒果商的人,还要把他认识的人再加入到搜索队列中。将一个人添加到队列需要的时间是固定的,即为O(1),因此对每个人都这样做需要的总时间为O(人数)。
所以,广度优先搜索的运行时间为O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数,E为边数。
练习
下面的小图说明了我早晨起床后要做的事情。
该图指出,我不能没刷牙就吃早餐,因此“吃早餐”依赖于“刷牙”。
另一方面,洗澡不依赖于刷牙,因为我可以先洗澡再刷牙。根据这个图,可创建一个列表,指出我需要按什么顺序完成早晨起床后要做的事情: (1) 起床 (2) 洗澡 (3) 刷牙 (4) 吃早餐
请注意,“洗澡”可随便移动,因此下面的列表也可行:
(1) 起床 (2) 刷牙 (3) 洗澡 (4) 吃早餐
6.3 请问下面的三个列表哪些可行、哪些不可行?
答:A不可行,吃早餐依赖于刷牙。C不可行,洗澡依赖于先起床。
6.4 下面是一个更大的图,请根据它创建一个可行的列表。
答:1.起床 2.刷牙 3.锻炼 4.洗澡5. 穿衣服 6. 吃早餐 7.打包午餐
课后答案: 1——起床,2——锻炼,3——洗澡,4——刷牙,5——穿衣服,6——打包午餐,7——吃早餐。
从某种程度上说,这种列表是有序的。如果任务A依赖于任务B,在列表中任务A就必须在任务B后面。这被称为拓扑排序,使用它可根据图创建一个有序列表。假设你正在规划一场婚礼,并有一个很大的图,其中充斥着需要做的事情,但却不知道要从哪里开始。这时就可使用拓扑排序来创建一个有序的任务列表。
假设你有一个家谱。
这是一个图,因为它由节点(人)和边组成。其中的边从一个节点指向其父母,但所有的边都往下指。在家谱中,往上指的边不合情理!(如下图)因为你父亲不可能是你祖父的父亲!
这种图被称为树。树是一种特殊的图,其中没有往后指的边。
6.5 请问下面哪个图也是树?
答:A,C是树,B有往上指的。C是一棵横着的树。树是图的子集,因此树都是图,但图可能是树,也可能不是。
6.6 小结
广度优先搜索指出是否有从A到B的路径。
如果有,广度优先搜索将找出最短路径。
面临类似于寻找最短路径的问题时,可尝试使用图来建立模型,再使用广度优先搜索来解决问题。
有向图中的边为箭头,箭头的方向指定了关系的方向,例如,rama→adit表示rama欠adit钱。
无向图中的边不带箭头,其中的关系是双向的,例如,ross - rachel表示“ross与rachel约会,而rachel也与ross约会”。
队列是先进先出(FIFO)的。
栈是后进先出(LIFO)的。
你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必须是队列。
对于检查过的人,务必不要再去检查,否则可能导致无限循环。