【算法】广度优先搜索——最短路径问题
目录
1. 广度优先搜索
广度优先搜索是一种用于图(图的介绍)的查找算法,可帮助回答两类问题。
- 第一类问题:从节点A出发,有前往节点B的路径吗?
- 第二类问题:从节点A出发,前往节点B的哪条路径最短?
当要计算地点A到地点B的最短路径时,你可以使用广度优先搜索。这个问题属于第二类问题:哪条路径最短?下面来详细研究这个算法,你将使用它来回答第一类问题:有路径吗?
假设你经营着一个芒果农场,需要寻找芒果经销商,以便将芒果卖给他。然而,你并不认识芒果经销商。为此,你可以在朋友中查找。
这种查找很简单。首先,创建一个朋友清单。
然后,一次检查名单中的每个人,看看他是否是芒果经销商。
假设你没有朋友是芒果经销商,那么你就必须在朋友的朋友中查找。
检查名单中的每个人时,你都将其朋友加入名单。
这样一来,不仅在朋友中查找,还在朋友的朋友中查找。别忘了,你的目标是在人际关系网中找到一位芒果经销商。因此,如果Alice不是芒果经销商,就将他的朋友也加入到名单中。这意味着你将她的朋友、朋友的朋友等中查找。使用这种算法将搜索你的整个人际关系网,直到找到芒果经销商。这就是广度优先搜索算法。
1.1 查找最短路径
再次重复,广度优先搜索可回答两类问题:
- 第一类问题:从节点A出发,有前往节点B的路径吗?
- 第二类问题:从节点A出发,前往节点B的哪条路径最短?
刚才你看到了如何回答第一类,下面来尝试回答第二类问题——谁是关系最近的芒果经销商。例如,朋友是一度关系,朋友的朋友是二度关系。
在你看来,一度关系胜过二度关系,二度关系胜过三度关系,以此类推。因此,你应先在一度搜索中搜索,确定没有芒果经销商后,才在二度关系中搜索。广度优先搜索就是这样做的!在广度优先搜索的执行过程中,搜索范围从起点开始逐渐向外延伸,即先检查一度关系,再检查二度关系。顺便问一句:将先检查Claire还是Anuj呢? Claire是一度关系,而Anuj是二度关系,因此将先检查Claire,后检查Anuj。
你也可以这样看,一度关系在二度关系之前加入查找名单。
你按顺序依次检查名单中的每个人,看看他是否是芒果经销商。这将先在一度关系中查找,再在二度关系中查找,因此找到的是关系最近的芒果经销商。广度优先搜索不仅查找从A到B的路径,而且找到的是最短的路径。
注意,只有按添加顺序查找时,才能实现这样的目的。换句话说,如果Claire先于Anuj加入名单就需要先检查Claire,再检查Anuj。如果Claire和Anuj都是芒果经销商,而你先检查Anuj再检查Claire,结果将会如何呢?中安到的芒果经销商并非是与你关系最近的,因为Anuj是你朋友的朋友,而Claire是你的朋友。因此,你需要按添加顺序进行检查。有一个可实现这种目的的数据结构,那就是队列(queue)。
1.2 队列
队列的工作原理与现实生活中的队列完全相同,假设你与朋友一起在公交车站排队,如果你排在他前面,你将先上车。队列的工作原理与此相同。队列类似于栈,你不能随机地访问队列中的元素。队列只支持两种操作:入队和出队。
如果你将两个元素加入队列,先加入的元素将在后加入的元素之前出队。因此,你可使用队列来表示查找名单!这样,先加入的人将先出队并先被检查。
队列是一种先进先出(First In First Out, FIFO)的数据结构,而栈是一种后进先出(Last In First Out,LIFO)的数据结构。
知道队列的工作原理之后,即可实现广度优先搜索。
2. 算法实现
先概述一下这种算法:
首先,创建一个队列。在Python中,可使用函数deque来创建一个双端队列。
from collection import deque
search_queue = deque() #创建一个队列
search_queue += graph["you"] #将你的邻居都加入到这个搜索队列中
别忘了,graph["you"]是一个数组,其中包含你的所有邻居,如["alice", "bob", "claire"]。这些邻居都将加入到搜索队列中。
下面来看看其他的代码。
while search_queue: #只要队列不为空
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结尾:如果是,他就是芒果销售商。这种判断方法有点搞笑,但就这个示例而言是可行的。下面来看看广度优先搜索的执行过程。
这个算法将不断执行,直到满足以下条件之一:
- 找到一位芒果供应商;
- 队伍变成空的,这意味着你的人际关系网中没有芒果经销商。
Peggy既是Alice的朋友又是Bob的朋友,因此她将被加入队列两次:一次是在添加Alice的朋友时,另一次是在添加Bob的朋友时。因此,搜索队列将包含两个Peggy。
但你只需要检查Peggy一次,看她是不是芒果经销商。如果你检查两次,就做了无用功。因此,检查完一个人后,应将其标记为已检查,且不再检查他。
如果不这样做,就可能会导致无限循环,假设你的人际关系网类似于下面这样。
一开始,搜索队列包含你的所有邻居。
现在你检查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_seller(person):
print person + " is a mango seller!"
return True
else:
search_queue += graph[person]
searched.append(person) #将这个人标记为检查过
return False
search("you")
3. 运行时间
如果你在你的整个人际关系网中搜索芒果销售商,就意味着你将沿每条边前进(记住,边是一个人到另一个人的箭头或连接),因此运行时间至少为O(边数)。
你还使用了一个队列,其中包含要检查的每个人,将一个人添加到队列需要的时间固定的,即为O(1),因此对每个人都需要的总时间为O(人数)。所以,广度优先搜索的运行时间为O(人数+边数),这通常写作O(V+E),其中V为顶点(vertice)数,E为边数。
上一篇: 最短路径(广度优先搜索)
下一篇: 密码的明文和密文显示