Bootstrap

代码随想录第六十天 | Bellman_ford队列优化算法(即SPFA) bellman_ford之判断负权回路 bellman_ford之单源有限最短路

Bellman_ford 队列优化算法(SPFA):

文章链接
题目链接:94.城市间货物运输Ⅰ

思路

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 之单源有限最短路:

文章链接
题目链接:96.城市间货物运输Ⅲ

思路

首先分析题目,在最多经过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后的节点,然后当轮松弛,处理全部上一轮的节点。
;