在图论中,有一类经典问题,就是最短路径问题。最短路径问题,又分为:
- 单源无权图最短路径问题
- 单源有权图最短路径问题
- 多源有权图最短路径问题
问题:啥叫单源,多源?
单源指的是给定一个起点,求到任意顶点的最短路径;多源指的是给定两个顶点,求这两个顶点的最短路径。
问题:啥叫有权,无权?
权,即权重,指的是两个邻接点的边的权重。无权的条件下,我们默认所有边的权重一致(给个默认值);有权条件下,每条边的权重不一致。这就导致了V1到V2也许经过的顶点数不是最少的,但可能是路径最短的,比如下图:
v1-v3经过的顶点最少,但路径不是最短的,最短路径是v1-v2-v3。
这篇文章,讨论单源无权图的最短路径,以下图为例展开讨论。
算法思想:
利用BFS广度优先算法,把路径按照递增的顺序入队,对第k个顶点,假设他的最短路是n,那么对k的(未访问过的)邻接节点来说,它们的最短路则是n+1,所以满足BFS满足路径递增要求。用一个dist 数组保存s到各顶点的最短路径。dist[i] 表示 从顶点s到i 的最短路径,默认为-1。
先把起点s压入队列,dist[s] = 0(自己到自己);
从队列中弹出一个顶点V,直到队列为空;遍历该顶点的所有未经访问的邻接结点(未经访问的结点dist[i] = -1,否则为最短路径),并更新邻接结点的dist为 dist[V]+1;
当队列为空时,已经求得s到任意顶点的最短路径。
步骤拆分分析:
先确认起点s,假设起点是V1,V1入队。
- 此时队列 [ v1 ]
从队列中弹出一个顶点:V1,访问V1的所有邻接点。
- V1的邻接点有V2、V4。V2、V4都没有访问过,更新他们的dist为dist[V1] + 1,并依次入队
- 此时队列 [ v2, v4 ]
从队列中弹出一个顶点:V2,V2的所有邻接结点的最短路径是 dist[V2]的基础上再加一条边,符合按照路径递增的顺序访问,先访问dist = 1的顶点,然后访问dist=2的顶点,.....。
- V2的邻接结点有V4、V5,由于V4已经访问过了,跳过V4。更新dist[V5] = dist[V2]+1,V5入队。
- 此时队列 [ v4, V5 ]
从队列中再弹出一个顶点:V4,接着跟上次的步骤一样。
- V4的邻接结点有V3、V5、V6、V7,由于dist[V5]不等于-1,表示已经访问过了,跳过V5。更新dist[V3/6/7] = dist[V4]+1,V3、V6、V7入队。
- 此时队列 [v5, v3, v6, v7]
从队列中再弹出一个顶点:V5,接着跟上次的步骤一样。
- V5的邻接结点有V7,但是由于V7已经访问过了,所以跳过
- 此时队列 [v3, v6, v7]
从队列中再弹出一个顶点:V3,接着跟上次的步骤一样。
- V3的邻接结点有V1、V6,但是由于V1和V6都访问过了,所以跳过
- 此时队列 [v6, v7]
从队列中再弹出一个顶点:V6,接着跟上次的步骤一样。
- V6的没有邻接结点,跳过
- 此时队列 [v7]
从队列中再弹出一个顶点:V7,接着跟上次的步骤一样。
- V7的邻接结点有V6,由于V6已经访问过了,跳过
- 此时队列 [],为空
此时判断队列为空,说明所有结点已经求得最短路径。
代码实现:
class UnweightBFS(BFSVisit):
def __init__(self):
super(UnweightBFS, self).__init__()
self.dist = []
def initVisted(self, g):
super(UnweightBFS, self).initVisted(g)
self.dist = [-1 for v in range(g.nV)] # 保存s到i的最短路径
def visit(self, g, v: int):
"""
最短路径
g: 图(矩阵实现)
v: 起点
"""
Q = QueueArr() # 初始化一个空队列 (我自己写的队列)
Q.addQ(v)
self.dist[v] = 0
while(not Q.isEmpty()):
V = Q.deleteQ()
for i, val in enumerate(g.G[V]):
if (self.dist[i] == -1 and g.G[V][i] > 0): # 未访问过,且是邻接节点(有边)
self.dist[i] = self.dist[V] + 1
Q.addQ(i)