算法导论之单源最短路径(Bellman-Ford和Dijkstra)

Bellman-Ford

一.Bellman-Ford算法的思想
Bellman-Ford算法(以下简称BF算法),用于解决边的权重可以为负值的单源最短路径,它通过对边进行松弛操作逐渐降低从源结点s到各结点v的最短路径估计值v.d,直到该估计值与实际的最短路径权重δ(s,v)相同。

二.Bellman-Ford算法介绍
准备阶段:一副赋值有向图,每个结点有两个属性:d和π,分别表示从源结点到该结点的最短路径和前驱结点

算法过程:初始化各结点,对每条边进行|V|-1次松弛处理,处理完后检查是否存在环路,返回t/f,算法中有两个重要的证明,一个是为何对每条边进行|V|-1次松弛后能够确保v.d=δ(s,v),另一个是为何v.d>u.d+w(u.v)时存在环路

(1)为何对每条边进行|V|-1次松弛后能够确保v.d=δ(s,v)?

最坏的情况下,从源结点到一个结点的最短路径需要经过所有结点、所有边,即每条边的最坏情况是需要|V|-1次松弛。

(2)为何v.d>u.d+w(u.v)时存在环路?

因为通过(1)我们知道在经过|V|-1次松弛之后能确保v.d=δ(s,v),那么v.d应该<=u.d+w(u,v),不可能出现v.d>u.d+w(u.v)的情况,如果出现只有可能存在环路,每次relax都会使v.d无限缩小。

三.Bellman-Ford算法伪代码

在给出BF算法伪代码之前,我们先给出BF算法需要用到的两个方法的伪代码

INITIALIZE_SINGLE_SOURCE(G,s)

1. for each vertex v∈G.V

2.     v.d=∞

3.     v.π=NULL

4. s.d=0

RELAX(u,v,w)

1. if v.d>u.d+w(u,v)

2.     v.d=u.d+w(u,v)

3.     v.π=u

BELLMAN_FORD(G,w,s)

1. INITIALIZE_SINGLE_SOURCE(G,s)

2. for i=1 to |G.V|-1

3.     for each edge(u,v)∈G.E

4.         RELAX(u,v,w)

5. for each edge(u,v)∈G.E

6.     if v.d>u.d+w(u.v)

7.         return FALSE

8. return TRUE

第1行,对结点的d和π值进行初始化;
第2-4行,我们对每条边进行|V|-1次松弛操作;
第5-7行,对每条边进行检查,检查是否存在权值为赋值的环路,如果存在环路,返回false;
第8行,返回true,说明存在最短路。

四.Bellman-Ford算法的复杂度

时间复杂度:初始化的时间为O(V),双重循环的时间为O((V-1)E),检查的时间为O(E),因此总时间为O(V+VE-E+E)=O((V+1)E)=O(VE)

五.算法图解
在这里插入图片描述
B站视频解释得特别好:Bellman-Ford算法详解

六.Bellman-Ford算法的代码

# 初始化 图 用字典表示层邻接链表
G = {1: {1: 0, 2: -3, 5: 5},
     2: {2: 0, 3: 2},
     3: {3: 0, 4: 3},
     4: {4: 0, 5: 2},
     5: {5: 0}}

# 输入图 获取信息
def getEdges(G):
    """ 输入图G,返回其边与端点的列表 """
    # 出发点
    v1 = []
    # 对应的相邻到达点
    v2 = []
    # 顶点v1到顶点v2的边的权值
    w = []
    # 遍历记录下基本信息
    for i in G:
        for j in G[i]:
            if G[i][j] != 0:
                w.append(G[i][j])
                v1.append(i)
                v2.append(j)
    # 返回三个信息
    return v1, v2, w

# 有权值为负的环报错的类
class CycleError(Exception):
    pass

# 定义Bellman-Ford算法  无穷大在这个代码中定义为999
def Bellman_Ford(G, v0, INF=999):

    # 获取图的信息
    v1, v2, w = getEdges(G)

    # 初始化源点与所有点之间的最短距离
    dis = dict((k, INF) for k in G.keys())
    # 起点定义距离为0
    dis[v0] = 0

    # 核心算法
    # 循环 n-1轮
    for k in range(len(G) - 1):
        # 用于标记本轮松弛中dis是否发生更新
        check = 0
        # 对每条边进行一次松弛操作
        for i in range(len(w)):
            # 如果起点加上边的长度小于起点到终点的长度
            if dis[v1[i]] + w[i] < dis[v2[i]]:
                # 更新起点到终点的长度
                dis[v2[i]] = dis[v1[i]] + w[i]
                # 如果有变化则将标记变量check定义1
                check = 1
        # check==0就可以说明没变化了
        if check == 0:
            # 退出循环
            break

    # 检测负权回路
    # 如果在 n-1 次松弛之后,最短路径依然发生变化,则该图必然存在负权回路
    flag = 0
    for i in range(len(w)):  # 对每条边再尝试进行一次松弛操作
        if dis[v1[i]] + w[i] < dis[v2[i]]:
            # 再进行松弛还可以变化
            flag = 1
            # 必然存在负权回路 退出训话
            break
    if flag == 1:
        # raise CycleError()
        # 返回false表示存在负权回路 不存在到该边的最短路径
        return False
    # 不然就将起点到每个点的最短路径返回
    return dis

# 定义起点为1
v0 = 1
# 调用函数
dis = Bellman_Ford(G, v0)
# 打印结果
print(dis.values())

Dijkstra

Dijkstra算法解决了有向图上带正权值的单源最短路径问题,其运行时间要比Bellman-Ford算法低,但适用范围比Bellman-Ford算法窄。

迪杰斯特拉提出的按路径长度递增次序来产生源点到各顶点的最短路径的算法思想是:对有n个顶点的有向连通网络G=(V, E),首先从V中取出源点u0放入最短路径顶点集合U中,这时的最短路径网络S=({u0}, {}); 然后从uU和vV-U中找一条代价最小的边(u*, v*)加入到S中去,此时S=({u0, v*}, {(u0, v*)})。每往U中增加一个顶点,则要对V-U中的各顶点的权值进行一次修正。若加进v作为中间顶点,使得从u0到其他属于V-U的顶点vi的路径不加v时最短,则修改u0到vi的权值,即以(u0, v*)的权值加上(v*, vi )的权值来代替原(u0, vi )的权值,否则不修改u0到vi的权值。接着再从权值修正后的V-U中选择最短的边加入S中,如此反复,直到U=V为止。

上面的说明都很抽象,下面图解算法思想:
在这里插入图片描述
寻找最短路径的过程如下:
在这里插入图片描述
对第一个图中的有向网络按以上算法思想处理,所求得的从源点F到其余顶点的最短路径的过程如图13.16所示。其中单圆圈表示U中的顶点,而双圆圈表示V-U中的顶点。连接U中两个顶点的有向边用实线表示,连接U和V-U中两个顶点的有向边用虚线表示。圆圈旁的数字为源点到该顶点当前的距离值。
初始时,S中只有一个源点F,它到V-U中各顶点的路径如图13.16(a)所示;选择图13.16(a)中最小代价边(F, B),同时由于路径(F, A)大于(F, B, A)和(F, C)大于(F, B, C),进行相应调整可得到图13.16(b);选择图13.16(b)中的最小代价边(B, C),同时由于(F, B, A)大于(F, B, C, A),进行相应调整可得到图13.16©;选择图13.16©中最小代价边(C, A)即可得到图13.16(d);选择图13.16(d)中最小代价边(F, D) 即可得到图13.16(e); 最后选择(F, E)即可得到图13.16( f )。

Dijkstra算法的代码:

# 此代码中定义无穷大为999999
MAX_value = 999999

# 定义dijkstra算法函数
def dijkstra(graph, s):
    # 判断图是否为空
    if graph is None:
        # 如果为空直接退出
        return None
    # 记录最终结果的一个列表
    dist = [MAX_value] * len(graph)
    # 起点距离为0
    dist[s] = 0
    # 记录已经搜索过的结点
    S = []
    # 记录待搜索的结点
    Q = [i for i in range(len(graph))]
    # 用来记录路径长度变化过程的列表
    dist_init = [i for i in graph[s]]
    # 进入未搜索的结点的列表的循环
    while Q:
        # enumerate() 函数用于将一个可遍历的数据对象(如列表、元组或字符串)组合为一个索引序列,
        # 同时列出数据和数据下标,一般用在 for 循环当中。
        # 循环结点和对应的路径长度 如果结点在Q中还未被遍历 则选出最短的长度赋给u_dist
        u_dist = min([d for v, d in enumerate(dist_init) if v in Q])
        # 利用索引搜索到这个结点
        u = dist_init.index(u_dist)

        # 搜索过的结点加入S中
        S.append(u)
        # 并且从Q中除去
        Q.remove(u)

        # 循环图中的结点和路径
        for v, d in enumerate(graph[u]):
            # 如果路径长度为正值并且能够到达
            if 0 < d < MAX_value:
                # 如果当前结点u的路径长度加上到v点的权重小于v点的路径长度
                if dist[v] > dist[u] + d:
                    # v点的路径长度更新
                    dist[v] = dist[u] + d
                    # dist_init里面也同步更新
                    dist_init[v] = dist[v]
                    
    # 返回最终结点  起点到各个结点的最短路径                
    return dist


if __name__ == '__main__':
    # 用邻接矩阵的形式表示图
    graph_list = [[0, 9, MAX_value, MAX_value, MAX_value, 14, 15, MAX_value],
                  [9, 0, 24, MAX_value, MAX_value, MAX_value, MAX_value, MAX_value],
                  [MAX_value, 24, 0, 6, 2, 18, MAX_value, 19],
                  [MAX_value, MAX_value, 6, 0, 11, MAX_value, MAX_value, 6],
                  [MAX_value, MAX_value, 2, 11, 0, 30, 20, 16],
                  [14, MAX_value, 18, MAX_value, 30, 0, 5, MAX_value],
                  [15, MAX_value, MAX_value, MAX_value, 20, 5, 0, 44],
                  [MAX_value, MAX_value, 19, 6, 16, MAX_value, 44, 0]]
    # 调用dijkstra函数 返回起点到每个结点的最短路径
    distance = dijkstra(graph_list, 0)
    # 打印结果
    print(distance)
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值