给定一个带权有向图G=(V,E),其中每条边的权是一个实数。另外,还给定V中的一个顶点,称为源。要计算从源到其他所有各顶点的最短路径长度。这里的长度就是指路上各边权之和。这个问题通常称为单源最短路径问题。

1.Floyd

Floyd是一种用于寻找图中每一对定点之间最短路径的算法。核心思想是对于每一对顶点u和v,寻找是否存在一个顶点w,使得从u到w再到v比已知的路径更短,如果是则更新它。

时间复杂度O(n3)
在这里插入图片描述

m, n = map(int, input().split())
graph = [[False] * m for _ in range(m)]
for _ in range(n):
    i, j, value = map(int, input().split())
    graph[i][j] = value
    graph[j][i] = value
def floyd(graph):
    for k in range(len(graph)):  # 更新次数  
        # 以下两层循环为遍历矩阵每个元素,根据递推关系更新每两个节点之间的路径权值
        for i in range(len(graph) - 1, -1, -1):
            for j in range(len(graph)):
                if graph[i][k] and graph[k][j] and (not graph[i][j] or graph[i][k] + graph[k][j] < graph[i][j]):
                    graph[i][j] = graph[i][k] + graph[k][j]
floyd(graph)
print(graph)

'''
6 8
0 1 3
0 3 2
1 2 1
2 3 8
2 4 12
3 4 3
3 5 4
4 5 2
'''
# out: [[4, 3, 4, 2, 5, 6], [3, 2, 1, 5, 8, 9], [4, 1, 2, 6, 9, 10], [2, 5, 6, 4, 3, 4], [5, 8, 9, 3, 4, 2], [6, 9, 10, 4, 2, 4]]

2.最小生成树

参考讲解

问题描述:
    输入:
    第一行输入两个整数,分别表示节点数m、边数n
    接下来总共有n行,每一行有三个数,前两个表示相连的节点编号(012...,n),最后一个数表示该边的权重值
    输出:
    一个整数,表示将所有点连起来权重的最小值。
    
示例:
输入:
9 15
0 1 3
0 5 4
1 2 8
1 6 6
1 8 5
2 8 2
2 3 12
3 8 11
3 6 14
3 7 6
3 4 10
4 7 1
4 5 18
5 6 7
6 7 9
输出:
36  # path: [(4, 7), (2, 8), (0, 1), (0, 5), (1, 8), (1, 6), (3, 7), (6, 7)]

克鲁斯卡尔算法(Kruskal)
1.对边集合进行排序;
2.选择最小权值边,若不构成环,则添加到集合边A中;
3.重复执行步骤2,直到添加|V| - 1 条边。

Kruskal基于边操作,适用于边少点多的场景,多用邻接表存储
时间复杂度:O(ElogE) or O(ElogV)

m, n = map(int, input().split())
a = [[] for _ in range(n)]
for i in range(n):
    a[i] = list(map(int, input().split())) # 邻接表存储
a.sort(key = lambda x: x[2])
res = 0
path = []
def find(father, f):
    while father[f] > 0:
        f = father[f]
    return f
father = {i : 0 for i in range(m)}  # 注意:字典的键是不可变的,可以是数字、字符串、元组,但不能为列表!!!
for i in range(n):
    node1 = find(father, a[i][0])
    node2 = find(father, a[i][1])
    if node1 != node2:
        father[node1] = node2
        res += a[i][2]
        path.append((a[i][0], a[i][1]))
    if len(path) == m - 1: break
print(res)
print(path)

Prim算法
Kruskal算法的过程为不断对子图进行合并,直到形成最终的最小生成树。Prim算法的过程则是只存在一个子图,
不断选择顶点加入到该子图中,即通过对子图进行扩张,直到形成最终的最小生成树。

算法过程如下:
1.按照距离子图的远近,对顶点集合进行排序;
2.选择最近的顶点加入到子图中,并更新相邻顶点对子图的距离;
3.重复步骤2,直到顶点集合为空。

prim基于顶点操作,适用于点少边多的场景,多用邻接矩阵存储.
时间复杂度:O(n**2)

m, n = map(int, input().split())
a = [[float('inf')] * m for _ in range(m)]  # 用邻接矩阵存储,Kruskal是邻接表存储!!!
for _ in range(n):
    i, j, v = map(int, input().split())
    a[i][j] = v
    a[j][i] = v
res, path = 0, []
print(a)
# weightE保存相关顶点间边的权值, V保存相关顶点下标
weightE, V = [0] * m, [0] * m
#将V0作为最小生成树的根开始遍历,权值为0
for i in range(1, m):
    weightE[i] = a[0][i]  # 将邻接矩阵第0行所有权值先加入数组
# 真正构造最小生成树的过程
for i in range(1, m):
    min_w = float('inf')
    j, k = 1, 0
    while j < m:  # 遍历全部顶点
        if weightE[j] and weightE[j] < min_w:
            min_w = weightE[j]
            k = j
        j += 1
    res += a[V[k]][k]
    path.append([V[k], k])
    weightE[k] = 0  # 将当前顶点的权值设置为0,表示此顶点已经完成任务,进行下一个顶点的遍历
    for j in range(1, m):
        if weightE[j] and a[k][j] < weightE[j]:
            weightE[j] = a[k][j]
            V[j] = k
print(res)
print(path)

3.迪杰斯特拉Dijkstra算法

参考讲解

Dijkstra算法主要针对的是有向图的单元最短路径问题,且不能出现权值为负的情况!Dijkstra算法类似于贪心算法计算从起点到指定顶点的最短路径,通过不断选择距离起点最近的顶点,来逐渐扩大最短路径权值,直到覆盖图中所有顶点(与方法4不同,其对边的松弛顺序是随意进行的)。其应用根本在于最短路径的最优子结构性质。Dijkstra算法步骤与Floyd算法类似,更新规则也是dist[j]=min(dist[j],dist[i]+matrix[i][j])

采用邻接矩阵存储,两顶点之间不存在有向连接,则设置为inf.

算法过程:
1.从未确认顶点中选择距离起点最近的顶点,并标记为已确认顶点
2.根据步骤 1 中的已确认顶点,更新其相邻未确认顶点的距离
3.重复步骤 1,2,直到不存在未确认顶点

# 图中的顶点数
V = 6
# 标记数组:used[v]值为False说明改顶点还没有访问过,在S中,否则在U中!
used = [False for _ in range(V)]
# 距离数组:distance[i]表示从源点s到i的最短距离,distance[s]=0
distance = [float('inf') for _ in range(V)]
# cost[u][v]表示边e=(u,v)的权值,不存在时设为INF
cost = [[float('inf') for _ in range(V)] for _ in range(V)]

def dijkstra(s):
    distance[s] = 0
    while True:
        # v在这里相当于是一个哨兵,对包含起点s做统一处理!
        v = -1
        # 从未使用过的顶点中选择一个距离最小的顶点
        for u in range(V):
            if not used[u] and (v == -1 or distance[u] < distance[v]):
                v = u
        if v == -1:  # 说明所有顶点都维护到S中了!
            break
        # 将选定的顶点加入到S中, 同时进行距离更新
        used[v] = True
        # 更新U中各个顶点到起点s的距离。之所以更新U中顶点的距离,是由于上一步中确定了k是求出最短路径的顶点,从而可以利用k来更新其它顶点的距离;例如,(s,v)的距离可能大于(s,k)+(k,v)的距离。
        for u in range(V):
            distance[u] = min(distance[u], distance[v] + cost[v][u])

if __name__ == '__main__':
    Inf = float('inf')
    cost = [[0, 1, 12, Inf, Inf, Inf],
            [Inf, 0, 9, 3, Inf, Inf],
            [Inf, Inf, 0, Inf, 5, Inf],
            [Inf, Inf, 4, 0, 13, 15],
            [Inf, Inf, Inf, Inf, 0, 4],
            [Inf, Inf, Inf, Inf, Inf, 0]]
    s = int(input('请输入一个起始点(0-5):'))
    dijkstra(s)
    print(distance)  # [0, 1, 8, 4, 13, 17]

4.贝尔曼-福特Bellman-Ford算法

参考讲解

算法核心是通过松弛函数进行距离更新:if d[v] > d[u] + w(u, v): d[v] = d[u] + w(u, v),最多通过|V - 1|次松弛就可以得到各顶点之间的最短距离。

787. K 站中转内最便宜的航班
在这里插入图片描述

class Solution:
    def findCheapestPrice(self, n: int, flights: List[List[int]], src: int, dst: int, K: int) -> int:
        distance = [[float('inf')] * (K + 2) for _ in range(n)]  # distance行表示点,列表示松弛次数,值代表最短距离
        for i in range(K + 2):
            distance[src][i] = 0  # 原点不管松弛几次距离本身都是0
        times = 1  # 松弛次数
        def relax(vi, vj, w, times):
            if distance[vj][times] > distance[vi][times - 1] + w:
                distance[vj][times] = distance[vi][times - 1] + w
        while times < K + 2:  # 我们这里把直连一个点设置为松弛1次,绕过一个点算松弛2次,绕过两个点算松弛3次,以此类推,可以绕过K个点,那么可以松弛K+1次
            for vi, vj, w in flights:
                if distance[vi][times - 1] != None:
                    relax(vi, vj, w, times)
            times += 1
        return distance[dst][K + 1] if distance[dst][K + 1] != float('inf') else -1

优化版本:

class Solution(object):
    def findCheapestPrice(self, n, flights, src, dst, k):
        dist = [float('inf')] * n  # dist[v]表示到达v的最小花费
        dist[src] = 0
        for i in range(k + 1):  # 对每条边做 k+1 次松弛操作,每次松弛操作实际上是对相邻节点的访问,所以总的中转为k次
            dist_old = [_ for _ in dist]
            for u, v, w in flights:
                dist[v] = min(dist[v], dist_old[u] + w)
        return dist[dst] if dist[dst] != float('inf') else -1

5.SPFA算法

参考讲解

SPFA(Shortest Path Faster Algorithm)算法是求单源最短路径的一种算法,它是Bellman-ford的队列优化,它是一种十分高效的最短路算法。

很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。SPFA的复杂度大约是O(kE),k是每个点的平均进队次数(一般的,k是一个常数,在稀疏图中小于2)。

但是,SPFA算法稳定性较差,在稠密图中SPFA算法时间复杂度会退化。

实现方法:
建立一个队列,初始时队列里只有起始点,再建立一个表格记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。然后执行松弛操作,用队列里有的点去刷新起始点到所有点的最短路,如果刷新成功且被刷新点不在队列中则把该点加入到队列最后。重复执行直到队列为空。

此外,SPFA算法还可以判断图中是否有负权环,即一个点入队次数超过N。

from collections import deque
class Solutions:
    def spfa(self, grids, src, target):
        dis = [float('inf')] * len(grids)
        counter = [0] * len(grids)
        dis[src] = 0
        visited = set()  #这里设置一个visited集合主要是为了后面判断是否存在负环,不需判断负环的代码请看下面简洁代码
        q = deque([(0, src)])
        visited.add(src)
        while q:
            d_cur, cur = q.popleft()
            visited.remove(cur)
            for node, value in enumerate(grids[cur]):
                if value and dis[cur] + value < dis[node]:
                    dis[node] = value + dis[cur]
                    if node not in visited:
                    	counter[node] += 1
                    	if counter[node] > len(grids):
                    		raise ValueError('输入有负环异常!')
                        visited.add(node)
                        q.append((dis[node], node))
        return dis[target] if dis[target] != float('inf') else -1

m, n, src, target = map(int, input().split())
grids = [[float('inf')] * m for _ in range(m)]
for _ in range(n):
    i, j, dis = map(int, input().split())
    grids[i][j] = dis
f = Solutions()
out = f.spfa(grids, src, target)
print(out)
'''
输入:
第一行节点数m、边数n、起点、终点,接下来n行每行三个数字,当前起点-下一终点-距离值。
输出起点到终点的最短距离。
input:
5 7 0 4
0 1 3
0 2 2
1 3 1
3 0 4
3 2 6
3 4 4
4 2 4
output: 
8
'''

简洁版:

from collections import deque
from collections import defaultdict
class Solution:
    def spfa(self, grids, src, target):
        dist = [float('inf')] * len(grids)
        dist[src] = 0
        dic = defaultdict(dict)
        for u, v, w in grids:
            dic[u][v] = w
        q = deque([src])
        while q:
            node = q.popleft()
            for v in dic.get(node, []):
                if dist[node] + dic[node][v] < dist[v]:
                    dist[v] = dist[node] + dic[node][v]
                    q.append(v)
        return dist[target] if dist[target] != float('inf') else -1

总结

就放一个各种算法对比表吧。
在这里插入图片描述

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐