抱佛脚Day03
文章目录
最短路问题是图论中最基础的问题,在程序设计竞赛试题中也经常出现。最短路是给定两个顶点,在以这两个点为起点和终点的路径中,边的权值和最小的路径。如果把权值当作距离,考虑最短距离的话就很容易理解了。智力游戏中的求解最少步数问题也可以说是一种最短路问题。
1.单源最短路问题 (Bellman-Ford算法)
单源最短路问题是固定一个起点,求它到其他所有点的最短路的问题。终点也固定的问题叫做两点之间最短路问题。但是因为解决单源最短路问题的复杂度也是一样的,因此通常当作单源最短路问题来求解。
记从起点 s s s出发到顶点 i i i的最短距离为 d [ i ] d[i] d[i]。则下述等式成立。
- d [ i ] = min ( d [ j ] + v a l ( j , i ) ∣ e ( j , i ) ∈ E ) d[i] = \min(d[j] + val(j, i)|e(j, i)\in \mathbb E) d[i]=min(d[j]+val(j,i)∣e(j,i)∈E)
如果给定的图是一个DAG,就可以按拓扑序给顶点编号,并利用用这条递推关系式计算出 d d d。但是,如果图中有圈,就无法依赖这样的顺序进行计算。在这种情况下,记当前到顶点 i i i最短路长度为 d [ i ] d[i] d[i],并设初值 d [ s ] = 0 , d [ i ] = i n f d[s]=0,d[i]=inf d[s]=0,d[i]=inf,再不断使用这条递推关系式更新 d d d的值,就可以算出新的 d d d。只要图中不存在负圈,这样的更新操作就是有限的。结束之后的 d d d是所求的最短距离了。
- 时间复杂度: O ( ∣ V ∣ × ∣ E ∣ ) O(|V|\times|E|) O(∣V∣×∣E∣)
例一
d [ A ] = 0 , d [ B ] = d [ C ] = d [ D ] = i n f d[A] = 0,d[B] = d[C] = d[D] = inf d[A]=0,d[B]=d[C]=d[D]=inf
d [ B ] = d [ A ] + 1 = 1 , d [ C ] = d [ A ] + 2 = 2 d[B] = d[A] + 1 = 1,d[C] = d[A] + 2 = 2 d[B]=d[A]+1=1,d[C]=d[A]+2=2
d [ D ] = min ( d [ B ] + 3 , d [ C ] + 1 ) = 3 d[D] = \min(d[B] + 3, d[C] + 1) = 3 d[D]=min(d[B]+3,d[C]+1)=3
numsV = 4
numsE = 4
edges = [edge(0, 1, 1),
edge(0, 2, 2),
edge(1, 3, 3),
edge(2, 3, 1)]
例二
class edge(object):
def __init__(self, start, end, cost):
self.start = start
self.end = end
self.cost = cost
def shortest_path(start, numsV, numsE):
d = [float("inf")] * numsV
d[start] = 0
while True:
update = False
for i in range(numsE):
e = edges[i]
if d[e.start] != float("inf") and d[e.end] > d[e.start] + e.cost:
d[e.end] = d[e.start] + e.cost
update = True
if update == False:
break
return d
def find_negative_loop(numsV, numsE):
d = [0] * numsV
for i in range(numsV):
for j in range(numsE):
e = edges[j]
if d[e.end] > d[e.start] + e.cost:
d[e.end] = d[e.start] + e.cost
if i == numsV - 1:
return True
return False
if __name__ == "__main__":
numsV = 2
numsE = 2
edges = [edge(0, 1, 1), edge(1, 0, 2)]
if find_negative_loop(numsV, numsE) == False:
d = shortest_path(0, numsV, numsE)
print(d)
else:
print("Exist negetive loop!")
2.单源最短路问题 (Dijkstra算法)
让我们考虑一下没有负边的情况。在 B e l l m a n − F o r d Bellman-Ford Bellman−Ford算法中,如果 d [ i ] d[i] d[i]还不是最短距离的话,那么即使进行 d [ j ] = d [ i ] + v a l ( j , i ) d[j]=d[i]+val(j,i) d[j]=d[i]+val(j,i)的更新, d [ j ] d[j] d[j]也不会变成最短距离。而且,即使 d [ i ] d[i] d[i]没有变化,每一次循环也要检査一遍从 i i i出发的所有边。这显然是很浪费时间的。因此可以对算法做如下修改。
- 找到最短距离已经确定的顶点,从它出发更新相邻顶点的最短距离。
- 此后不需要再关心上一条中的“最短距离已经确定的顶点”。
写法一:枚举所有的顶点来查找下一个使用的顶点
class edge(object):
def __init__(self, start, end, cost):
self.start = start
self.end = end
self.cost = cost
def dijkstraInit(numsV, edges):
cost = [[float("inf")] * numsV for _ in range(numsV)]
for edge in edges:
cost[edge.start][edge.end] = edge.cost
cost[edge.end][edge.start] = edge.cost
d = [float("inf")] * numsV
used = [False] * numsV
return cost, d, used
if __name__ == "__main__":
numsV = 4
numsE = 4
edges = [edge(0, 1, 1),
edge(0, 2, 2),
edge(1, 3, 3),
edge(2, 3, 1)]
cost, d, used = dijkstraInit(numsV, edges)
d[0] = 0
while True:
v = -1
for u in range(numsV):
if not used[u] and (d[u] < d[v] or v == -1):
v = u
if v == -1: # 顶点都使用过了
break
used[v] = True
for u in range(numsV):
d[u] = min(d[u], d[v] + cost[v][u])
print(d)
写法二:堆
import heapq
import collections
class edge(object):
def __init__(self, start, end, cost):
self.start = start
self.end = end
self.cost = cost
def dijkstraInit(numsV, start, edges):
graph = collections.defaultdict(list)
for edge in edges:
graph[edge.start].append([edge.end, edge.cost])
graph[edge.end].append([edge.start, edge.cost])
que = []
d = [float("inf")] * numsV
d[start] = 0
heapq.heappush(que, [0, start])
return graph, que, d
if __name__ == "__main__":
numsV = 4
numsE = 4
edges = [edge(0, 1, 1),
edge(0, 2, 2),
edge(1, 3, 3),
edge(2, 3, 1)]
graph, que, d = dijkstraInit(numsV, 1, edges)
while len(que) != 0:
item = heapq.heappop(que)
v = item[1]
if d[v] < item[0]:
continue
for i in range(len(graph[v])):
e = graph[v][i]
if d[e[0]] > d[v] + e[1]:
d[e[0]] = d[v] + e[1]
heapq.heappush(que, [d[e[0]], e[0]])
print(d)
3.任意两点间的最短路问题(Floyd-Warshall算法)
求解所有两点间的最短路的问题叫做任意两点间的最短路问题。让我们试着用DP来求解任意两点间的最短路问题。
import heapq
import collections
class edge(object):
def __init__(self, start, end, cost):
self.start = start
self.end = end
self.cost = cost
if __name__ == "__main__":
numsV = 4
numsE = 4
edges = [edge(0, 1, 1),
edge(0, 2, 2),
edge(1, 3, 3),
edge(2, 3, 1)]
d = [[float("inf")] * numsV for _ in range(numsV)]
for i in range(numsV):
d[i][i] = 0
for edge in edges:
d[edge.start][edge.end] = edge.cost
d[edge.end][edge.start] = edge.cost
for k in range(numsV):
for i in range(numsV):
for j in range(numsV):
d[i][j] = min(d[i][j], d[i][k] + d[k][j])
print(d)
4.路径还原
截至目前,我们都只是在求解最短距离。虽然许多问题只需输出最短距离就可以了,但是也有的问题需要求解最短路的路径。我们以Dijkstra算法为例,试着来求解最短路径。在求解最短距离
时,满足
d
[
j
]
=
d
[
k
]
+
c
o
s
t
[
k
]
[
j
]
d[j] = d[k] + cost[k][j]
d[j]=d[k]+cost[k][j]的顶点k, 就是最短路上顶点j的前趋节点,因此通过不断寻找前趋节点就可以恢复出最短路。时间度杂度是
O
(
∣
E
∣
)
O(|E|)
O(∣E∣),如果用prev[j]来记录最短路上顶点j的前趋,那么就可以在
O
(
∣
V
∣
)
O(|V|)
O(∣V∣)的时间内完成最短路的恢复。
def get_path(t):
path = []
while t != -1:
path.append(t)
t = prev[t]
return path.reverse()