Bellman_ford 队列优化算法(SPFA):
思路
bellman_ford算法是对图中所有边进行n - 1次松弛,但是实际上是有一些多余的操作的。
实际上只要对更新过miniDist的节点所连接的边进行松弛即可,使用队列保存更新过miniDist的节点。因为会出现松弛的边的尾节点已经在队列中
因此我们使用visited数组记录节点是否入队。
每次出队一个节点,visited设置为False,然后将其相连的边进行松弛操作,如果边的尾节点已经在队列中,那么不入队;不在则入队,同时设置visited为True
from collections import deque
class Edge():
def __init__(self, to, val):
self.to = to # 指向的节点
self.val = val # 权值
def print_miniDist(miniDist, n):
for i in range(1, n + 1):
print("i: " + str(i) + "miniDist[i]: " + str(miniDist[i]))
def SPFA(graph, n, start, end):
miniDist = [float("inf")] * (n + 1) # 节点编号从1开始
miniDist[start] = 0
que = deque()
que.append(start) # 队列存储更新过miniDist的节点
visited = [False] * (n + 1) # 标记在队列中的节点
visited[start] = True
while que:
curnode = que.popleft()
visited[curnode] = False # visited只标记在队列中的节点,出队列要标记为False
# 遍历该节点连接的节点
for edge in graph[curnode]:
# 进行松弛操作
if miniDist[curnode] + edge.val < miniDist[edge.to]:
miniDist[edge.to] = miniDist[curnode] + edge.val # 记得更新miniDist
if not visited[edge.to]: # 将松弛后不在队列中的节点入队
visited[edge.to] = True # 标记入队
que.append(edge.to)
# print("curnode: " + str(curnode))
# print_miniDist(miniDist, n)
if miniDist[end] == float("inf"):
print("unconnected")
else:
print(miniDist[end])
def main():
n, m = map(int, input().split())
graph = [[] for _ in range(n + 1)] # 节点编号从1开始
for _ in range(m):
s, t, v = map(int, input().split())
graph[s].append(Edge(t, v))
SPFA(graph, n, 1, n)
if __name__ == '__main__':
main()
可能会对while que
有疑问,如果图中存在环,while循环是否会死循环,如果图中存在环,那么可以分为正权环和负权环。
如果存在正权环的话,那么while循环到后面会因为不存在能够松弛的边而退出;存在负权环的话,会一直存在能够松弛的边,while会死循环。而题目已经给出不存在负权环了,因此不用担心while会死循环
bellman_ford 之判断负权回路:
文章链接
[题目链接:95.城市间货物运输Ⅱ]
思路
首先分析题目,题目中可能存在负权回路,要求:如果存在负权回路,输出circle;如果从1到n没有路径,输出unconnected;如果从1到n有路径且不存在负权回路,输出最短路径长度。
那么本题比之前的bellman_ford多了需要判断负权回路,在之前bellman_ford算法中,需要对所有的边松弛n - 1次,得到从源点到其它节点的最短路径,如果再对所有边松弛一次的话,最短路径长度不会变,即miniDist数组不变,而如果存在负权回路。
以上图为例,从1到3不存在最短路径,因为1可以绕负权回路无数圈,而miniDist[1]可以一直降低,因此即使对图上所有边松弛n次,miniDist还是会发生变化。
从而本题的思路是,在原来bellman_ford的基础上,将松弛n - 1次改为n次,并在最后一次松弛判断miniDist是否发生变化。
"""
下面这段代码,读入和保存数据耗费时间太长,会导致超时
"""
def main():
n, m = map(int, input().split())
edges = [] # 保存边
for _ in range(m):
s, t, v = map(int, input().split())
edges.append([s, t, v])
# 松弛n次
start, end = 1, n
miniDist = [float("inf")] * (n + 1)
miniDist[start] = 0
flag = False # 是否存在负权回路
for i in range(1, n + 1): # 松弛n次,最后一次判断负权回路
for edge in edges:
s, t, v = edge[0], edge[1], edge[2]
if i < n:
if miniDist[s] != float("inf") and miniDist[s] + v < miniDist[t]:
miniDist[t] = miniDist[s] + v
else: # 多加一次判断负权回路
if miniDist[s] != float("inf") and miniDist[s] + v < miniDist[t]:
flag = True
break
if flag:
print("circle")
elif miniDist[end] == float("inf"):
print("unconnected")
else:
print(miniDist[end])
if __name__ == '__main__':
main()
"""
用下面这种读入数据的方式不会超时
"""
import sys
def main():
input = sys.stdin.read
data = input().split()
index = 0
n = int(data[index])
index += 1
m = int(data[index])
index += 1
grid = []
for i in range(m):
p1 = int(data[index])
index += 1
p2 = int(data[index])
index += 1
val = int(data[index])
index += 1
# p1 指向 p2,权值为 val
grid.append([p1, p2, val])
当然,也可以用SPFA算法
图中每个节点最多入队n-1次,因此只要使用数组记录每个节点的入队次数,当入队次数达到n时,认为存在负权回路。
还需要修改的地方在:1. 只要miniDist被更新了,就将其入队 2. 如果最后发现有入队次数达到n的,那么需要将队列中元素全部出队,然后再跳出for循环
from collections import deque
class Edge:
def __init__(self, to, val):
self.to = to
self.val = val
def main():
n, m = map(int, input().split())
graph = [[] for _ in range(n + 1)] # 邻接表
for _ in range(m):
s, t, v = map(int, input().split())
graph[s].append(Edge(t, v))
start, end = 1, n
que = deque([start])
miniDist = [float("inf")] * (n + 1)
miniDist[start] = 0
visited = [False] * (n + 1)
visited[start] = True
count = [0] * (n + 1) # 记录节点入队的次数
flag = False
while que:
curnode = que.popleft()
visited[curnode] = False # 已经不在队列中了
for edge in graph[curnode]:
# 松弛
if miniDist[curnode] + edge.val < miniDist[edge.to]:
miniDist[edge.to] = miniDist[curnode] + edge.val
que.append(edge.to)
count[edge.to] += 1
if count[edge.to] == n:
flag = True
while que: que.popleft() # 将队列清空
break
if flag:
print("circle")
elif miniDist[end] == float("inf"):
print("unconnected")
else:
print(miniDist[end])
if __name__ == '__main__':
main()
bellman_ford 之单源有限最短路:
思路
首先分析题目,在最多经过k个城市的条件下,从城市src到城市dst的最低运输成本。
- 最多k个:也就是经过的城市个数可以少于k个,但是要最短。
思路演变 - bellman_ford中松弛1次,miniDist得到的是从源点出发有一条路径的最短距离,因此在本题中,需要限定bellman_ford松弛的次数,最多经过k个城市,那么路径最多为k+1条。
- 题目中没说不存在负权回路,那么还是存在负权回路的,会对结果产生影响。负权回路的影响,以下面这个输入为例
注意最后一行是src, dst, k
松弛第一次
miniDist[1] = 0
边(1,2):miniDist[1] + val < miniDist[2]
边(2, 3):miniDist[2] + val < miniDist[3]
边(3, 1):miniDist[3] + val < miniDist[1]
边(3, 4):miniDist[3] + val < miniDist[4]
虽然只松弛了一次,但是发现所有节点的miniDist都更新了,包括节点1,但是这明显不对,miniDist[1] 被更新为了-1,松弛一次miniDist应当是源节点经过一条路径到目标节点的最短距离,而miniDist[1]的路径是1→2→3→1,不是一条路径了。
问题出现在更新(3, 1)这条边时,使用的是本轮松弛得到的miniDist,所以对1来说,相当于多松弛了,而本题限定了松弛次数,所以松弛的时候应当采用上一轮的miniDist才对 - 在每次松弛前,使用miniDist_copy保存上一轮的miniDist数组的值,而松弛条件的判断是
if miniDist_copy[i] != float("inf") and miniDist_copy[curnode] + val < miniDist[i]
,最后是**<miniDist[i]**的,因为一个节点可能有多条边指向,miniDist要选择其中最小的。
代码
def main():
n, m = map(int, input().split())
edges = []
for _ in range(m):
s, t, v = map(int, input().split())
edges.append([s, t, v])
start, end, k = map(int, input().split()) # 这里要记得
miniDist = [float("inf")] * (n + 1)
miniDist[start] = 0
miniDist_copy = []
for _ in range(k + 1): # 松弛k+1次
update = False
miniDist_copy = miniDist[:] # 记录上一轮松弛后的miniDist
for s, t, v in edges:
if miniDist_copy[s] != float("inf") and miniDist_copy[s] + v < miniDist[t]:
miniDist[t] = miniDist_copy[s] + v
update = True
if not update:
break
if miniDist[end] == float("inf"):
print("unreachable")
else:
print(miniDist[end])
if __name__ == '__main__':
main()
需要注意的是本题不是从1到n的最短距离,而是从src到dst的最短距离,限定经过城市数为k
为什么之前的bellman_ford题目不需要保存上一轮松弛的miniDist数组呢,因为之前的bellman_ford,要么没有负权回路和轮数限制,那么松弛次数再多,不会对结果产生影响;要么有负权回路,没有轮数限制,但是如果有负权回路只要输出circle就行,不需要在负权回路下仍然求最短路径长度
SPFA
在本题应用SPFA的关键在于要记录松弛的次数,用的技巧是,每次que队列保存上一次松弛后更新miniDist的节点,然后每次松弛对队列中上一次的节点所连的边进行松弛,同时入队本次松弛时更新miniDist数组的值。
同时为了避免目前这轮松弛时,入队的更新了miniDist的节点在这一轮重复入队,每轮松弛使用visited记录入队的节点。
同bellman_ford相同,也要用到miniDist_copy保存上一轮松弛的结果
from collections import deque
class Edge:
def __init__(self, to, val):
self.to = to
self.val = val
def print_miniDist(miniDist, n):
for i in range(1, n + 1):
print("i: " + str(i) + "miniDist[i]: " + str(miniDist[i]))
def main():
n, m = map(int, input().split())
graph = [[] for _ in range(n + 1)]
for _ in range(m):
s, t, v = map(int, input().split())
graph[s].append(Edge(t, v))
start, end, k = map(int, input().split()) # 这里要记得
miniDist = [float("inf")] * (n + 1)
miniDist[start] = 0
miniDist_copy = []
que = deque()
que.append(start)
while k != -1 and que:
miniDist_copy = miniDist[:]
visited = [False] * (n + 1) # 标记本轮松弛时入队的节点,避免本轮重复入队
que_size = len(que) # 记录上一轮更新过的miniDist,作为该轮松弛的起点
for _ in range(que_size):
curnode = que.popleft()
for edge in graph[curnode]:
# 松弛节点
if miniDist_copy[curnode] + edge.val < miniDist[edge.to]:
miniDist[edge.to] = miniDist_copy[curnode] + edge.val
if not visited[edge.to]:
visited[edge.to] = True
que.append(edge.to)
k -= 1
#print_miniDist(miniDist, n)
if miniDist[end] == float("inf"):
print("unreachable")
else:
print(miniDist[end])
if __name__ == '__main__':
main()
学习收获:
- bellamn_ford队列优化算法(又叫SPFA):使用队列保存已经更新过miniDist的节点,同时只松弛更新过miniDist节点相连的边
- bellman_ford 判断负权回路:如果不存在负权回路的话,松弛n轮后miniDist结果不变,以此来判断是否存在负权回路;而SPFA的话,则是记录节点入队的次数,只要更新了miniDist,就将节点入队
- bellman_ford 单源有限最短路:明确限定了松弛的轮数,如果按照之前的来,那么松弛一轮时实际上对某个节点松弛了超过一次,而且本题要求输出结果(即使存在负权回路),因此需要保存上一轮的结果;而SPFA通过队列保存上一轮更新miniDist后的节点,然后当轮松弛,处理全部上一轮的节点。