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字典中的值就不会再变好了
Q | A | B | C | D | E |
---|---|---|---|---|---|
d | 0 | inf | inf | inf | inf |
removed | 10 | 3 | inf | inf | |
removed | 7 | removed | 11 | 5 | |
removed | 7 | removed | 11 | removed | |
removed | removed | removed | 9 | removed |
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=∣V∣TextractMin+∣E∣TDecreaseKey
第一部分是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),这也是最佳的方案。
**二叉堆和菲不那契堆的算法复杂度,老师也没解释,要看一下这两个数据结构,应该就能够理解了。