最短路径问题总结。
适用于单源点、非负权值的图。
基本思想是基于贪心法,执行过程描述如下:
初始化字典dist表示源点到任一顶点的距离。dist[源点]=0,其余INF。然后用一个集合set保存已经被访问的元素。从源点开始,对该顶点所有的出边进行松弛,然后加入到被访问集合中,然后在dist字典中寻找下一个值最小的顶点,依次进行直到所有顶点已经加入被访问集合中。
以上是最朴素的思想,可以用一个优先级队列进行优化。
代码实现如下:
def Dijkstra(G, vo):
old = set()
view = vo
INF = float('inf')
dis = dict((k, INF) for k in G.keys()) # dis[k]表示到k的最短距离
dis[vo] = 0
path={}
while len(old) < len(G):
old.add(view) # 当前考虑的节点
for w in G[view]:#遍历view周围的节点,进行松弛
if dis[w] > dis[view] + G[view][w]:
dis[w] = dis[view] + G[view][w]
path[w]=view#path[w]记录w的上一个结点的下标
new =INF
for n in dis.keys():#找下一个节点k,要求dis[k]最小且k未被松弛过。
if n in old:
continue
if dis[n]<=new: #必须小于等于,因为如果一个顶点没出边,for循环转一圈没有意义,view会一直是这个顶点.
view=n
new=dis[n]
for x in path:
print('到顶点',x,'的路径为',end=': ')
t=x
while t!=None:
print(t,"<-",end=' ')
t=path.get(t)
print('总长度为',dis[x])
return dis,path
这里G代表图,vo代表起点。图的表示使用嵌套的字典表示的,即{‘A’:{‘B’:3}}表示顶点A有A到B的边,权值为3.
适用于单源非负环图。
思想描述如下:
初始化dist字典同上,松弛每条边(等价于松弛所有顶点)顶点-1轮,如果之后松弛还能成功,表明有负环存在,无法求解。
def Bellman_Ford(G,v):
'''
每一轮对所有顶点进行松弛,如果哪一轮松弛后最短距离都不变化就提前结束。否则,如果执行n
轮之后还在变化则说明存在负环。最多n-1次松弛就可以得到答案。(n为顶点个数)
:param G: 图的邻接表
:param v: 起点
:return: dis,path
'''
dis=dict((k,float('inf'))for k in G)
dis[v]=0
path={}
for rounds in G:#一共执行n次,如果n次之后最短路径还没求出来说明有负环
changed=False
#对所有顶点进行松弛
for u in G:
for w in G[u]:
if dis[w] > dis[u] + G[u][w]:
dis[w] = dis[u] + G[u][w]
path[w] = u
changed=True
if not changed:
break
else:
sys.exit('存在负环,算法求不出来')
return dis,path
即用队列优化的Bellman-Ford算法,考虑上述算法,显然只有前面的边松弛成功,后面的边松弛才会成功。所以上面的实现用了个标志用于提前结束,这里用一个队列,把松弛成功的顶点的所有邻接顶点加入其中,每次从队列取出顶点进行松弛。
def SPFA(G,v):
'''
利用 spfa 算法判断负环有两种方法:
1) spfa 的 dfs 形式,判断条件是存在一点在一条路径上出现多次。
2) spfa 的 bfs 形式,判断条件是存在一点入队次数大于总顶点数。
:param G: 图
:param v: 起点
:return: dis,path
'''
#用队列优化Bellman-Ford算法,这里不能有负环,程序中没有判断。
q=deque()
path={}#用于打印路径
dis=dict((k,float('inf'))for k in G)#记录距离
dis[v]=0
times=dict((k,0) for k in G)#用于记录每个顶点入队列的次数,如果大于顶点个数表明有负环
q.append(v)#起始把起点入队
while len(q)!=0:
x=q.popleft()#出队一个
#对该顶点所有的邻边进行松弛
for w in G[x]:
if dis[w]>dis[x]+G[x][w]:
dis[w]=dis[x]+G[x][w]
path[w]=x #记录此顶点的上一个顶点
if w not in q:
if times[w]>len(G):
sys.exit('存在负环,嗝屁了')
q.append(w)
times[w]+=1
return dis,path
基于DP的一个算法,代码即是递推式。
思想就是u到v的最短路径经不经过k,这也解释了为什么k是最外层循环(自底向上运算,后面的需要用到前面的结果)
def Folyd_Warshall(G):
G2=deepcopy(G)
#强行补成邻接矩阵的形式
for u in G2:
for v in G2:
if u==v:
G2[u][v]=0
if G2[u].get(v)==None:
G2[u][v]=float('inf')
#算法开始
for k in G:
for u in G:
for v in G:
if G2[u][v]>G2[u][k]+G2[k][v]:
G2[u][v]=G2[u][k]+G2[k][v]
return G2
这个算法利用了Bellman和Dijkstra算法。
由于Bellman算法限制,同样不能用于负环图。
其本质思想是:
给图加上一个顶点s,然后令s到其所有顶点距离为0,利用Bellman-Ford算法,求出s到其它顶点的最短距离记为h(h[a]表示s到a的最短距离),然后对图中所有的边进行重新赋权(目的消除负权,好使用Dijkstra算法),具体体现为若有顶点u、v和边G[u][v]
,则G[u][v]+=h[u]-h[v]
。之后,去除顶点s即可。然后对所有顶点执行Dijkstra算法。最后在已经求解的结果上别忘了恢复原来的权值,即Dist[u][v]+=h[v]-h[u]
,这里Dist表示u到v的最短距离。
可以证明,重新赋权图求得的最短路径和原始图一样,(值不一样可以通过恢复操作来求出)。
def johnson(G):
G=deepcopy(G)
s=object()
G[s]={v:0 for v in G} #新节点到所有其它顶点距离为0
h,_=Bellman_Ford(G,s) #h表示s到其它节点的最短路径值
del G[s] #可以删除s节点了
for u in G: #对于从u到v的边,进行重新赋权
for v in G[u]:
G[u][v]+=h[u]-h[v]
D={}
for u in G:
D[u]=Dijstra(G,vo=u)[0]
for v in G:
D[u][v]+=h[v]-h[u]
return D
def print_path(dis,path):
for x in path:
print('到顶点',x,'的总长度为:',dis[x],'具体的路径为',end=': ')
t=x
while t!=None:
print(t,"<-",end=' ')
t=path.get(t)
print()
G1={'B': {'D': 3, 'E': 0, 'C': 1},
'D': {'B': 0, 'C': 2},
'E': {'D': 0},
'C': {},
'A': {'B': 1, 'C': 3}}
G={0:{1:5,2:3},
1:{0:5,3:1,4:3,5:6},
2:{0:3,4:8,5:7,6:6},
3:{1:1,7:6,8:8},
4:{1:3,2:8,7:3,8:5},
5:{1:6,2:7,8:3,9:3},#5
6:{2:6,8:8,9:4},#6
7:{3:6,4:3,10:2,11:2},#7
8:{3:8,4:5,5:3,6:8,11:1,12:2},#8
9:{5:3,6:4,11:3,12:3},#9
10:{7:2,13:3,14:5},#10
11:{7:2,8:1,9:3,13:5,14:2},#11
12:{8:2,9:3,13:6,14:6},#12
13:{10:3,11:5,12:6,15:4},#13
14:{10:5,11:2,12:6,15:3},#14
15:{13:4,14:3}#15
}