第六章 广度有限搜索
1 图简介
假设你居住在旧金山,要从双子峰前往金门大桥。你想乘公交车前往,并希望换乘最少。可乘坐的公交车如下。
从双子峰出发,可沿下面的路线三步到达金门大桥。其他的都需要四步。
这种问题被称为最短路径问题(shortest-path problem),解决最短路径问题的算法被称为广度优先搜索。
如何解决路径问题,需要两个步骤:
①使用图来建立问题模型
②使用广度优先搜索解决问题
2 图是什么
图模拟一组连接。比如打牌,谁欠谁钱。可以这样表示:
Alex欠Rama钱,Tom欠Adit钱,等等。图由节点(node,图中圈)和边(edge,图中指向性的线段)组成。一个节点可能与众多节点直接相连,这些节点被称为邻居
3 广度优先搜索
广度优先搜索是一种用于图的查找算法,可解决两类问题:
①从节点A出发,有前往节点B的路径吗?
②从节点A出发,前往节点B的哪条路径最短?
广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系。广度优先搜索不仅查找从A到B的路径,而且找到的是最短的路径。
你需要按添加顺序进行检查。有一个可实现这种目的的数据结构,那就是队列(queue)。
4 队列
队列类似于栈,你不能随机地访问队列中的元素。队列只支持两种操作:入队和出队。
如果你将两个元素加入队列,先加入的元素将在后加入的元素之前出队。因此,你可使用队列来表示查找名单!这样,先加入的人将先出队并先被检查。
队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last InFirst Out,LIFO)的数据结构。
练习
对于下面的每个图,使用广度优先搜索算法来找出答案。
6.1 找出从起点到终点的最短路径的长度。
最短路径的长度是2
6.2 找出从cab到bat的最短路径的长度。
最短路径的长度是2
5 实现图
图由多个节点组成,每个节点都与相邻节点相连,散列表可以很好的表示这种关系。
散列表让你能够将键映射到值。在这里,你要将节点映射到其所有邻居。如下图:
代码表示:
graph = {} graph["you"] = ["alice", "bob", "claire"]
对于更复杂的呢?
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"] = []
这被称为有向图(directed graph),其中的关系是单向的。因此,Anuj是Bob的邻居,但Bob不是Anuj的邻居。无向图(undirected graph)没有箭头,直接相连的节点互为邻居。
6 实现算法(芒果商)
算法的工作原理:
首先,创建一个队列,可用deque函数创建一个双端队列
from collections import 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"] = [] search_queue = deque() search_queue += graph["you"] def person_is_seller(name): #判断是否是个芒果商,用名字的m判断,是为了实现代码 return name[-1] == 'm' while search_queue: #列表不为空 person = search_queue.popleft() #popleft返回队列左边的第一个 if person_is_seller(person): #进行判断,用定义的函数 print(person + " is a mango seller!")else: search_queue += graph[person] #不是的话继续递归
下面是广度搜索的执行过程:
这个算法不断执行,直到满足以下条件:
①找到一位芒果销售商;
②队列变成空的,这意味着你的人际关系网中没有芒果销售商。
这里可能出现无限循环,因为搜索队列将在包含你和包含Peggy之间反复切换。因此,修改代码:
from collections import 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"] = [] search_queue = deque() search_queue += graph["you"] def person_is_seller(name): return name[-1] == 'm' def search(name): search_queue = deque() search_queue += graph[name] searched = [] #这个数组用于记录检查过的人 while search_queue: person = search_queue.popleft() if not person in searched: #仅当这个人没检查过时才检查 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")
有关运行时间:因为要搜索每一个人,因此每条边都要走过即O(边数),同时,还要对于每一个人要检查,将这个人添加到队列需要时间O(1),因为要对每一个人则是O(人数)。
所以,广度优先搜索的运行时间为O(人数 + 边数),这通常写作O(V + E),其中V为顶点(vertice)数,E为边数。
练习:
下面的小图说明了我早晨起床后要做的事情
该图指出,我不能没刷牙就吃早餐,因此“吃早餐”依赖于“刷牙”。
另一方面,洗澡不依赖于刷牙,因为我可以先洗澡再刷牙。根据这个图,可创建一个列表,指出我需要按什么顺序完成早晨起床后要做的事情:(1) 起床(2) 洗澡(3) 刷牙(4) 吃早餐
请注意,“洗澡”可随便移动,因此下面的列表也可行:(1) 起床(2) 刷牙(3) 洗澡(4) 吃早餐
6.3 请问下面的三个列表哪些可行、哪些不可行?
AC不可行,B可行
6.4 下面是一个更大的图,请根据它创建一个可行的列表。
1——起床,2——锻炼,3——洗澡,4——刷牙,5——穿衣服,6——打包午餐,7——吃早餐。
从某种程度上来说,这种列表是有序的。任务A依赖于任务B,在列表中任务A就必须在任务B后面。这被称为拓扑排序,使用它可根据图创建一个有序列表。
还有一种结构:树,是一种特殊的图,其中没有往后指的边。
6.5 请问下面哪个图也是树?
AC是数,B不是。树是图的子集,因此树都是图,但图可能是树,也可能不是。
7 小结
广度优先搜索指出是否有从A到B的路径。如果有,广度优先搜索将找出最短路径。
面临类似于寻找最短路径的问题时,可尝试使用图来建立模型,再使用广度优先搜索来解决问题。
有向图中的边为箭头,箭头的方向指定了关系的方向,例如,rama→adit表示rama欠adit钱。
无向图中的边不带箭头,其中的关系是双向的,例如,ross - rachel表示“ross与rachel约会,而rachel也与ross约会”。
队列是先进先出(FIFO)的。栈是后进先出(LIFO)的。
你需要按加入顺序检查搜索列表中的人,否则找到的就不是最短路径,因此搜索列表必须是队列。
对于检查过的人,务必不要再去检查,否则可能导致无限循环。