2、Bellman-Ford算法和SPFA算法、Floyd算法
Bellman-Ford算法
Dijkstra算法可以很好地解决无负权图的最短路径问题,但如果出现了负权边,Dijkstra算法就会失效。为了更好地求解有负权边的最短路径问题,需要使用Bellman-Ford算法(简称BF算法),和Dijkstra算法一样,Bellman-Ford算法可以解决单源最短路径问题,但也能处理有负权边的情况。Bellman-Ford算法的思路简洁直接,易于读者掌握。
根据图中环的边权和的正负,可以将环分为零环、正环、负环。显然,图中的零环和正环不会影响最短路径的求解,而负环则不然。但是如果图中的负环无法从源点到达,则最短路径的求解不会受到影响。
## 样例输入
5 6 0 2
1 2 1 5 3
0 1 1
0 2 2
0 3 1
1 2 1
2 4 1
3 4 1
class Node:
def __init__(self,v,dis):
## v为邻接边的目标顶点
## dis为邻接边的边权
self.v=v
self.dis=dis
def Bellman(s):
global d,w,nums,pre
## 起点s到达自身的距离为零
d[s]=0
w[s]=weight[s]
nums[s]=1
## 以下为求解数组d的部分
## 由于最短路径树的层数不会超过V层
## 因此Bellman-Ford算法的松弛操作不会超过V-1轮
## 执行n-1轮操作,n为顶点数
for i in range(n-1):
## 每轮操作都遍历所有边
for u in range(n):
for j in range(len(Adj[u])):
## 邻接边的顶点
v=Adj[u][j].v
## 邻接边的边权
dis=Adj[u][j].dis
## 以u为中介点可以使d[v]更小
if d[u]+dis<d[v]:
## 进行松弛操作
d[v]=d[u]+dis
w[v]=w[u]+weight[v]
nums[v]=nums[u]
pre[v].clear()
pre[v].append(u)
else:
if d[u]+dis==d[v]:
if w[u]+weight[v]>w[v]:
w[v]=w[u]+weight[v]
pre[v].append(u)
## 重新统计nums[v]
## nums[v]+=nums[u]
nums[v]=0
## Bellman-Ford算法期间会多次访问曾访问过的顶点
uset=set(pre[v])
for it in uset:
nums[v]+=nums[it]
## 判断负环的代码
## 对每条边进行判断
for u in range(n):
for i in range(len(Adj[u])):
## 邻接边的顶点
v=Adj[u][j].v
## 邻接边的边权
dis=Adj[u][j].dis
## 如果仍可以松弛
if d[u]+dis<d[v]:
## 说明存在负环
return False
return True
n,m,s,e=map(int,input().strip().split())
## 输入点权
weight=list(map(int,input().strip().split()))
## 邻接表
Adj=[[] for _ in range(n)]
## 输入边权
for _ in range(m):
u,v,w=map(int,input().strip().split())
Adj[u].append(Node(v,w))
Adj[v].append(Node(u,w))
## d存储最短路径
d=[float("inf")]*n
## w存储点权和
w=[0]*n
## nums存储最短路径数量
nums=[0]*n
## 前驱数组pre
pre=[[] for _ in range(n)]
Bellman(s)
print(nums[e])
print(w[e])
## 样例输出
2
4
SPFA算法
虽然Bellman-Ford算法的思路很简洁,但是O(VE)的时间复杂度确实很高,在很多情况下并不尽人意。注意到,只有当某个顶点u的d[u]值改变时,从它出发的边的邻接点v的d[v]才可能被改变。因此可以进行一个优化:建立一个队列,每次将队首顶点u弹出,然后对从u出发的所有边u-v进行松弛操作,如果成立则进行覆盖d[v],此时如果v不在队列中,就把v加入队列。这样操作直至队列为空时(说明图中无源点可达的负环),或是某个顶点的入队次数超过V-1时(说明图中存在源点可达负环)。这种优化后的算法被称为SPFA算法,它的期望时间复杂度是O(KE),E是边数,K常是不大于2的常数。
## 样例输入
5 6 0 2
1 2 1 5 3
0 1 1
0 2 2
0 3 1
1 2 1
2 4 1
3 4 1
class Node:
def __init__(self,v,dis):
## v为邻接边的目标顶点
## dis为邻接边的边权
self.v=v
self.dis=dis
from collections import deque
def SPFA(s):
global d,inq,nums,w,num
dq=deque()
## 源点入队
dq.append(s)
inq[s]=True
nums[s]+=1
d[s]=0
w[s]=weight[s]
num[s]=1
while (len(dq)!=0):
## 队首顶点编号u
u=dq.popleft()
inq[u]=False
## 遍历u的所有邻接边v
for j in range(len(Adj[u])):
v=Adj[u][j].v
dis=Adj[u][j].dis
## 松弛操作
if d[u]+dis<d[v]:
d[v]=d[u]+dis
w[v]=w[u]+weight[v]
num[v]=num[u]
## 如果v不在队列中
if inq[v]==False :
dq.append(v)
inq[v]=True
nums[v]+=1
if nums[v]>=n:
## 存在可达负环
return False
else:
if d[u]+dis==d[v]:
if w[u]+weight[v]>w[v]:
w[v]=w[u]+weight[v]
num[v]+=num[u]
return True
n,m,s,e=map(int,input().strip().split())
## 输入点权
weight=list(map(int,input().strip().split()))
## 邻接表
Adj=[[] for _ in range(n)]
## 输入边权
for _ in range(m):
u,v,w=map(int,input().strip().split())
Adj[u].append(Node(v,w))
Adj[v].append(Node(u,w))
## d存储最短路径
d=[float("inf")]*n
## w存储点权和
w=[0]*n
## num最短路径
num=[0]*n
## 入队次数
nums=[0]*n
## 标记是否入队
inq=[False]*n
SPFA(s)
print(num[e])
print(w[e])
## 样例输出
2
4
Floyd算法
Floyd算法(弗洛伊德算法)用来解决全源最短路径问题,时间复杂度O(n**3)。由于n***3的复杂度决定了顶点数n的限制约在200以内,因此使用邻接矩阵来实现Floyd算法是非常合适且方便的。
Floyd算法基于这样一个事实:如果存在顶点k,使得以k作为中介点时顶点i和顶点j的当前距离缩短,则使用顶点k作为顶点i和顶点j的中介点。即当dis[i][k]+dis[k][j]<dis[i][j],则令dis[i][j]=dis[i][k]+dis[k][j]。
## 样例输入
6 8
0 1 1
0 3 4
0 4 4
1 3 2
2 5 1
3 2 2
3 4 3
4 5 3
def Floyd():
for k in range(n):
for i in range(n):
for j in range(n):
if dis[i][k]!=float("inf") and dis[i][k]+dis[k][j]<dis[i][j]:
dis[i][j]=dis[i][k]+dis[k][j]
n,m=map(int,input().strip().split())
dis=[[float("inf")]*n for _ in range(n)]
for i in range(n):
dis[i][i]=0
for _ in range(m):
u,v,w=map(int,input().strip().split())
dis[u][v]=w
Floyd()
for i in range(n):
for j in range(n):
print(dis[i][j],end=" ")
print()
## 样例输出
0 1 5 3 4 6
inf 0 4 2 5 5
inf inf 0 inf inf 1
inf inf 2 0 3 3
inf inf inf inf 0 3
inf inf inf inf inf 0