[算法分析笔记]最短路径(上)- Dijkstra算法

1. 问题定义

在一个图中寻找最短路径的问题可以定义如下:
G r a p h ( V , E ) Graph(V,E) Graph(V,E)中:

  • V V V是所有顶点的集合
  • E E E是边的集合,E中的每个元素用一对顶点集合表示,比如 ( A , B ) (A,B) (A,B)就表示从顶点 A A A B B B的一条有向边。
  • W W W是映射条边的权重的方法, W ( E ) = W e i g h t W(E)=Weight W(E)=Weight
    u , v u,v u,v G r a p h ( V , E ) Graph(V,E) Graph(V,E)中的任意两个顶点,寻找从 u u u v v v的最短的路径。
    这个问题是有一个前提提条件,即不存在权重是负值的边。加入负值权重的边会使问题复杂化,有时我们甚至会发现最短路径可能是不存在的,此处暂不做讨论

2. 代码解析

废话不多说,先上Dijkstra算法原码,然后我们再来证明其正确性。

2.1 代码

以下是用python实现的Dijkstra算法:

class graph:
    def __init__(self, V=[], W={}):
        self.V = V
        self.W = W
        
    def weight(self, vertix_1, vertix_2):
        if self.W.get(vertix_1,'NaN') != 'NaN':
            if self.W[vertix_1].get(vertix_2,'NaN') != 'NaN':
                return self.W[vertix_1][vertix_2]
        return float('-inf')

    def Adj(self, u):
        adj_u = []
        for key in self.W[u]:             
                adj_u.append(key)
        return adj_u

def ExtractMin(Q, d):
    minvalue = float('inf')
    u = ''
    #print(d)
    for item in Q:
        if d[item] < minvalue:
            u = item
            minivalue = d[item]
    Q = [v for v in Q if v != u]
    return u, Q

def DijkstraAlg(G,s):
    # Initialization
    d = {}    
    for v in G.V:
        d[v] = float('inf')  
    d[s] = 0
    S=[]
    Q = [v for v in G.V]
    #Start search here
    i_loop = 0
    while len(Q) != 0:
        i_loop += 1
        u, Q = ExtractMin(Q, d)
        S.append(u)
        adj_u = G.Adj(u)
        for v in adj_u
            if d[v] > d[u] + G.weight(u,v):
                d[v] = d[u] + G.weight(u,v)  
    return d

2.2 图的数据结构说明。

定义一个class graph,包括2个属性,V是所有顶点的集合,V的数据类型是列表。
W是所有权重有向边的集合,为了方便后面查询,W的数据结构设计成了一个用字典实现的树形结构。举例A顶点出发有两条边,一条指向C,权值是5, 另一条指向E,权值是2。可以表示成

W = {'A':{'C':5, 'E':2}}

graph类还有个方法weight(vertix_1, vertix_2),它的两个参数分别是起始顶点和结束顶点。如果两个顶点之间有一条直接相连的边,则返回该边的权值,否则返回无穷大。
graph类的另外一个方法Adj(u)可以返回从u出发相邻的所有顶点的集合,注意此处时必须从u出发的可以到达的,也就是说边的方向时从u指向另一个顶点的。

2.3 算法主体说明

Dijkstra算法的主函数就是DijkstraAlg(G,s),它的第一个参数是graph类型的对象,定义了图的结构。第二个参数s是该图中的某一个顶点。而该函数的返回值是一个字典,字典中的每个key是图中除s以外的其他顶点,key对应的值就是从s到该顶点的最短路径权值。
举例:我们的出发顶点是A,而图中除A以外还有C,D,E,F顶点。那么我们得到的

d = {C:3, D: 7, E: 2, F: 5}

这里的3,7,2,5是从A分别到C,D,E,F的最短路径权值。

该函数的第一部分是初始化,将字典对象d里所有key的值都设成了无穷大。而出发顶点s的值则为0。

同时初始化一个空列表S和一个V列表的拷贝列表Q。初始状态是所有顶点都存在Q列表中,我们后面会一个一个取出来处理后,再把该顶点从Q挪到S。

接下来就是算法的正篇了,一个循环语句,当Q中元素非空则进入循环。
第一步是调用ExtractMin函数来获得一d字典中的最小值对应的key u,并将该值对应的顶点从Q列表中去除。因此,每次调用ExtractMin时都会遍历Q列表中存在的顶点。

接下来我们调用Adj方法获得与u相邻的顶点列表adj_u,并对adj_u中每个顶点元素v比较d[v] > d[u] + G.weight(u,v),如果该条件成立则更新d[v] = d[u] + G.weight(u,v)。 因为d[v]里记录的是当前从A出发到v的最短路径的权值,他们最初都是无穷的,当我们发现一个比目前记录值更小的值,就更新d[v]的值为该值。这个操作被称为松弛(relaxation)

就这样,当Q队列值为空时,我们就完成了从A顶点出发到所有其他顶点的最短路径的搜索,并存储在d字典中返回。

2.4 运算实例

接下来我们以一个简单的图作为例子,来看一下以上算法是如何运作的。

在这里插入图片描述

我们将其表示成graph类要求的格式

V=['A','B','C','D','E']
W = {'A':{'B':10,'C':3},'B':{'C':1,'D':2},'C':{'B':4,'D':8,'E':2},'D':{'E':7},'E':{'D':9}}
G = graph(V,W)
d = DijkstraAlg(G,'A')
print('The shortest path from A to each vertex is:')
print(d)

在最外圈的while循环时,每次d字典的值变化如下表。这里黄色高亮的是每次ExtractMin时找到的顶点,该顶点将从Q队列里被移除。而该顶点对应在d字典中的值就不会再变好了

QABCDE
d0infinfinfinf
removed103infinf
removed7removed115
removed7removed11removed
removedremovedremoved9removed

3. 算法有效性证明

这个算法看起来是不是有点悬,怎么这样就得到了最短路径呢?
让我们分三步来证明其正确性。

3.1 第一步,证明在初始化后任何时候d[v]大于等于s到v的最短路径 δ ( s , v ) \delta(s,v) δ(s,v)

证明:初始化后, d [ s ] = 0 d[s] = 0 d[s]=0,其余d[x]皆为无穷大。
s到s的最短路径 δ ( s , s ) = 0 \delta(s,s)=0 δ(s,s)=0,故 d [ s ] > = δ ( s , s ) d[s]>=\delta(s,s) d[s]>=δ(s,s)成立。其余d[v]为无穷大, d [ v ] > = δ ( s , v ) d[v]>=\delta(s,v) d[v]>=δ(s,v)也成立。

假设,经过一次的松弛操作(relaxation)是 d [ v ] = d [ u ] + w ( u , v ) d[v] = d[u]+w(u,v) d[v]=d[u]+w(u,v),得到存在 d [ v ] < δ ( s , v ) d[v]<\delta(s,v) d[v]<δ(s,v)
那么 d [ u ] + w ( u , v ) < δ ( s , v ) d[u]+w(u,v)<\delta(s,v) d[u]+w(u,v)<δ(s,v),由此可以推出:
d [ u ] + w ( u , v ) > = δ ( s , u ) + w ( u , v ) d[u]+w(u,v)>=\delta(s,u)+w(u,v) d[u]+w(u,v)>=δ(s,u)+w(u,v)
由于w(u,v)是连接u和v的一条边,故
w ( u , v ) > = δ ( u , v ) w(u,v)>=\delta(u,v) w(u,v)>=δ(u,v),
也就w是说要么(u,v)边是最短路径,或者存在另外一条更短的路径,由此可得,
δ ( s , u ) + w ( u , v ) > = δ ( s , u ) + δ ( u , v ) = δ ( s , v ) \delta(s,u)+w(u,v) >= \delta(s,u)+\delta(u,v)=\delta(s,v) δ(s,u)+w(u,v)>=δ(s,u)+δ(u,v)=δ(s,v)
此处,产生矛盾,根据前面假设 d [ v ] < δ ( s , v ) d[v]<\delta(s,v) d[v]<δ(s,v),但此处却得到 d [ v ] > = δ ( s , v ) d[v]>=\delta(s,v) d[v]>=δ(s,v)。故而,前面的假设不成立,必然有 d [ v ] > = δ ( s , v ) d[v]>=\delta(s,v) d[v]>=δ(s,v)

3.2 第二部,我们需要证明如下引理(lemma)假设,s->…->u->v是一条最短路径,如果 d [ u ] = δ ( s , u ) d[u]=\delta(s,u) d[u]=δ(s,u),那么在我们对 ( u , v ) (u,v) (u,v)执行了松弛操作,那么 d [ v ] = δ ( s , v ) d[v]=\delta(s,v) d[v]=δ(s,v)

证明: δ ( s , v ) = w ( s − > . . . − > u ) + w ( u , v ) = δ ( s , u ) + w ( u , v ) \delta(s,v)=w(s->...->u)+w(u,v)=\delta(s,u)+w(u,v) δ(s,v)=w(s>...>u)+w(u,v)=δ(s,u)+w(u,v)

由第一步我们已经证明了 d [ v ] > = δ ( s , v ) d[v]>=\delta(s,v) d[v]>=δ(s,v), 那么存在以下两种情况
情况一 d [ v ] = δ ( s , v ) d[v]=\delta(s,v) d[v]=δ(s,v),此处则无需松弛操作,已满足题目要求。
情况而 d [ v ] > δ ( s , v ) d[v]>\delta(s,v) d[v]>δ(s,v),此时我们对(u,v)作松弛操作,另
d [ v ] = d [ u ] + w ( u , v ) = δ ( s , v ) d[v]=d[u]+w(u,v)=\delta(s,v) d[v]=d[u]+w(u,v)=δ(s,v),满足题目要求。

3.3 第三步,证明当Dijkstra算法终止时, 所有的 d [ v ] = δ ( s , v ) d[v]=\delta(s,v) d[v]=δ(s,v)

证明:当v被从Q队列里移除,加入到S队列里时,d[v]不再会发生变化,即: d [ v ] = δ ( s , v ) d[v]=\delta(s,v) d[v]=δ(s,v)
我们假设当v被加入到S队列中时,而u尚未被加入到S列表中的,且会被选中作为下一个加入S队列。根据ExtractMin算法, d [ u ] > d [ u ′ ] d[u]>d[u'] d[u]>d[u],u’是所有尚未被加入到S列表中的顶点。
反证法,假设此时 d [ u ] < > δ ( s , u ) d[u]<>\delta(s,u) d[u]<>δ(s,u),由此可推出
d [ u ] > δ ( s , u ) , δ ( s , u ) 是 最 短 路 径 , 因 此 d [ u ] 不 可 能 比 它 小 d[u]>\delta(s,u),\delta(s,u)是最短路径,因此d[u]不可能比它小 d[u]>δ(s,u)δ(s,u)d[u]
d [ u ] d[u] d[u]此时不是最短路径,那么存在一条从s到u且不经过v的最短路径p,则 w ( p ) = δ ( s , u ) w(p)=\delta(s,u) w(p)=δ(s,u) w ( p ) w(p) w(p)表示所有p经过的边的权值和。
考虑p上存在这样一条边 ( x , y ) (x,y) (x,y),其中x属于S列表,y属于Q列表,且 ( x , y ) (x,y) (x,y)是p路径上第一次跨越S和Q两个区域的边。
由于x在S区域,所以 d [ x ] = δ ( s , x ) d[x]=\delta(s,x) d[x]=δ(s,x),根据算法可知,当我们将x加入S队列是,我们对所有和x相邻顶点进行松弛操作。对 y进行松弛操作时,由之前第二部引理可以得到 d [ y ] = d [ x ] + w ( x , y ) = δ ( s , y ) d[y]=d[x]+w(x,y)=\delta(s,y) d[y]=d[x]+w(x,y)=δ(s,y)
由于,y是从s到u的最短路径p上的一个顶点,所以必然有 δ ( s , y ) < δ ( s , u ) , d [ y ] < δ ( s , u ) \delta(s,y) < \delta(s,u), d[y]<\delta(s,u) δ(s,y)<δ(s,u)d[y]<δ(s,u)
如果在ExtractMin中选择将d[y]加入S列表中,而不是d[u],意味着此时d[y]<d[u],这与之前的假设, d [ u ] > d [ u ′ ] d[u]>d[u'] d[u]>d[u],u’是所有尚未被加入到S列表中的顶点,矛盾。显然,u和y此时都未加入S列表,如果d[y]<d[u],则根据算法u就不会被选中加入S列表。
因此,只能是 d [ u ] > δ ( s , u ) d[u]>\delta(s,u) d[u]>δ(s,u)时,u才会被选中加入S列表中。
证毕

3.4 算法复杂度分析

Kijkstra算法的时间复杂度可以分为两部分来算。
T i m e = ∣ V ∣ T e x t r a c t M i n + ∣ E ∣ T D e c r e a s e K e y Time = |V|_{T_{extractMin}}+|E|_{T_{DecreaseKey}} Time=VTextractMin+ETDecreaseKey
第一部分是ExtractMin方法用于寻找最小值所耗费的时间,这个时间取决于有多少个顶点数。
第二部分是对每一条边进行松弛操作的时间,这个时间取决于有多少条边。
对于 T e x t r a c t M i n T_{extractMin} TextractMin T D e c r e a s e K e y T_{DecreaseKey} TDecreaseKey的运算时间则取决于你使用的数据结构。
如果使用数组(Array),那么算法复杂度就是 O ( V 2 ) O(V^2) O(V2),因为此时 T e x t r a c t M i n T_{extractMin} TextractMin的时间复杂的是 O ( V 2 ) O(V^2) O(V2),而 T D e c r e a s e K e y T_{DecreaseKey} TDecreaseKey是常数级。
如果你使用的是二叉堆(Binary heap),那么算法复杂度就是 O ( ( V + E ) l g V ) O((V+E)lgV) O((V+E)lgV)
如果你使用的是菲不那契堆(Fibonacci heap),那么算法复杂度就是 O ( E + V l o g V ) O(E+VlogV) O(E+VlogV),这也是最佳的方案。
**二叉堆和菲不那契堆的算法复杂度,老师也没解释,要看一下这两个数据结构,应该就能够理解了。

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

编程小白的逆袭日记

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值