6 广度优先搜索
广度优先搜索(breadth-first search,BFS),广度优先搜索能够找出两样东西之间的最短距离,例如:
① 编写国际跳棋AI,计算最少走多少步可获胜;
② 编写拼写检查器,计算最少编辑多少个地方就可将错拼的单词改写成正确的单词,如将READED改为READER需要一个编辑的地方;
③根据你的人际关系网络找到关系最近的医生。
这种问题被称为最短路径问题(shortest-path problem),解决这种问题的算法被称为广度优先搜索。
解决该问题需要两个步骤
① 使用图来建立问题模型。
② 使用广度优先搜索解决问题。
广度优先搜索可以回答两个问题
① 第一类问题:从节点A出发,有前往节点B的路径吗?
(在你的人际关系中,有芒果销售商吗?)
② 第二类问题:从节点A出发,前往节点B的哪条路径最短?
(哪个芒果销售商与你的关系最近?)
一度关系胜过二度关系,二度关系胜过三度关系,以此类推。你应先在一度关系中搜索,确定其中没有芒果销售商后,才在二度关系中搜索。
队列 Queue
如果你将几个元素加入队列,先加入的元素将在后加入的元素之前出队,队列只支持两种操作:出队和入队。
队列是一种先进先出(First In First Out,FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。
示例
在你的朋友圈中,找到芒果销售商。
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
search_queue=deque() //<---------创建一个队列
search_queue+=graph["you"] //<---------将你的邻居添加到这个队列中
while search_queue: //<---------只要队列不为空
person=search_queue.popleft() //<---------取出一个人
if person_is_seller(person): //<---------检查这个人是否是芒果销售商
print(person+" is a mango seller ! ") //<-是芒果销售商
return True
else:
search_queue.append(graph[person]) //<-----不是芒果销售商。将这个人的朋
//search_queue+=graph[person] 友都加入队列
return False //<-----如果达到了这里,就说明队列中没人是芒果销售商
这个算法将不断执行,直到满足以下条件之一
- 找到一位芒果销售商;
- 队列变成空的,这意味着你的人际关系网中没有芒果销售商。
Peggy即使Alice的朋友又是Bob的朋友,因此她将被加入队列两次:一次是在添加Alice的朋友时,另一次是在添加Bob朋友时。因此,搜索队列将包含两个Peggy。你只需检查Peggy一次,看她是不是芒果销售商。如果检查两次,就做了无用功。因为检查完一个人后,应将其标记为已检查,且不再检查他。(检查一个人之前,要确认之前没检查过他,这很重要。为此,你可使用一个列表来记录检查过的人。)
注意:对于检查过的人,务必不要再去检查,否则可能会导致无限循环。
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_a_seller(person):
print(person + " is a seller ! ")
else:
search_queue.append(graph[person]) //<---search_queue+=graph[person]
searched.append(person)
return False
search("you")
运行时间
如果你在你的整个人际关系网中搜索芒果销售商,就意味着你将沿每条边前行(记住,边是从一个人到另一个人的箭头或连接),因此运行时间至少为O(边数)。
你还使用了一个队列,其中包括要检查的每个人。将一个人添加到队列需要的时间是固定的,即为O(1),因此对每个人都这样做需要的总时间为O(人数)。所以广度优先算法运行时间为O(人数+边数),这通常写作为O(V+E),其中V为顶点(vertice)数,E为边数。
7 狄克斯特拉算法
广度优先算法(breadth-first search,BFS):找出段数最少的路径。
狄克斯特拉算法(Dijkstra’s algorithm):找出最快的路径。
使用广度优先搜索来查找两点之间的最短路径,那时“最短路径”的意思是段数最少。在狄克斯特拉算法中,你给每段都分配了一个数字或权重,因此狄克斯特拉算法找出的是总权重最小的路径。
狄克斯特拉算法包含的4个步骤:
① 找出最便宜的节点,即可在最短时间内前往的节点。
② 对于该节点的邻居,检查他们是否有前往它们的更短路径,如果有,就更新其开销。
③ 重复这个过程,直到对图中的每个节点都这样做了。
④ 计算最终路径。
狄克斯特拉算法用于每条边都有关联数字的图,这些数字成为权重(weight)。带权重的图成为加权图(weighted graph),不带权重的图称为非加权图(unweighted graph)。
权重:
加权图: 非加权图:
要计算非加权图中的最短路径,可使用广度优先搜索。要计算加权图中的最短路径,可使用狄克斯特拉算法。图还可能有环,狄克斯特拉算法只适用于有向无环图(directed acyclicgraph,DAG)。
狄克斯特拉算法关键理念:找出图中最便宜的节点,并确保没有到该节点的更便宜的路径!
最短路径不一定指的是物理距离,也可能是让某种度量指标最小。
负权边:权重出现负数的情况。如果出现负权边,就不能使用狄克斯特拉算法。
因为狄克斯特拉算法有这样的假设:对于处理过的节点,没有前往该节点的更短路径。这种假设仅在没有负权边时成立。因此,不能将狄克斯特拉算法用于包含负权边的图。在包含负权边的图中,要找出最短路径,可使用另一种算法——贝尔曼-福德算法(Bellman-Ford algorithm)(本书不介绍)。
如何用代码实现狄克斯特拉算法:
示例:
要编写解决这个问题,需要三个列表。
随着算法的进行,你将不断更新散列表costs和parents。
//=========================== 表示整个图的散列表graph={} ============================//
graph={}
graph["start"]={} //<----------"start"为起始节点(起点)
graph["start"]["a"]=6 //<----------"start"→"a"的权重为6
graph["start"]["b"]=2 //<----------"start"→"b"的权重为2
//------------------------------------------------------------------------------------//
>>>print(graph)
>{'start': {'a': 6, 'b': 2}}
>>>print(graph["start"])
>{'a': 6, 'b': 2}
>>>print(graph["start"].keys())
>dict_keys(['a', 'b'])
>>>print(graph["start"]["a"])
>6
>>>print(graph["start"]["b"])
>2
//------------------------------------------------------------------------------------//
graph["a"]={} //<----------"a"为中间节点
graph["a"]["fin"]=1 //<----------"a"→"fin"的权重为1
graph["b"]={} //<----------"b"为中间节点
graph["b"]["a"]=3 //<----------"b"→"a"的权重为3
graph["b"]["fin"]=5 //<----------"b"→"fin"的权重为5
graph["fin"]={} //<----------"fin"为终止节点(终点)
//------------------------------------------------------------------------------------//
>>>print(graph)
>{'start': {'a': 6, 'b': 2}, 'a': {'fin': 1}, 'b': {'a': 3, 'fin': 5}, 'fin': {}}
//============================== 创建开销列表costs={} ===============================//
infinity=float("inf")
costs={}
costs["a"]=6
costs["b"]=2
costs["fin"]=infinity
>>>print(costs)
>{'a': 6, 'b': 2, 'fin': inf}
//================================ 储存父节点的散列表 ===============================//
parents={}
parents["a"]="start"
parents["b"]="start"
parents["fin"]=None
//======== 你需要一个数组,用于记录处理过的节点,因为对同一个节点,你不可处理多次。 =======//
processed=[]
//======================================= 算法 =====================================//
node=find_lowest_cost_node(costs) //<----在未处理的节点中找出开销最小的节点
while node is not None: //<----while循环在所有节点被处理后结束
cost=costs[node]
neighbors=graph[node]
for n in neighbors.keys():
new_cost=cost+neighbors[n]
if costs[n]>new_cost:
costs[n]=new_cost
parents[n]=node
processed.append(node)
node=find_lowest_cost_node(costs)
//============================= find_lowest_cost_node() ============================//
def find_lowest_cost_node(costs):
lowest_cost=float("inf")
lowest_cost_node=None
for node in costs:
cost=costs[node]
if cost<lowest_cost and node not in processed:
lowest_cost=cost
lowest_cost_node=node
return lowest_cost_node
//------------------------------------------------------------------------------------//
注意:
Python中表示正负无穷用下面代码:
float("inf")
、float("-inf")
小结:
(1)广度优先搜索用于非加权图中查找最短路径。
(2)狄克斯特拉算法用于在加权图中查找最短路径。
(3)仅当权重为正时,狄克斯特拉算法才管用。
(4)如果图中包含负权边,请用贝尔曼-福德算法。