算法训练营day60 图论⑩ Bellman_ford 队列优化算法、判断负权回路、单源有限最短路(修改后版本)

        增加对最短路径的优化算法、负权回路、单源有限最短的讲解

Bellman_ford 队列优化算法

--------------------------------------------------------------------------------

        8.24更新:该算法是针对带负值的最短路径的优化算法,核心通过队列来实现,代码中visit数组来标记在队列中的元素,以防重复入队,每次循环一个节点的出度,即自该节点出发的边,模拟过程中队列判断n次(均由队列实现,具体次数无循环语句要求)

--------------------------------------------------------------------------------

        我们之前的松弛算法,是对所有边进行松弛,真正有效的松弛,只有松弛 边(节点1->节点2) 和 边(节点1->节点3)。而松弛 边(节点4->节点6),边(节点5->节点3)等等 都是无效的操作,因为 节点4 和 节点 5 都是没有被计算过的节点。所以 Bellman_ford 算法 每次都是对所有边进行松弛,其实是多做了一些无用功。

        只需要对 上一次松弛的时候更新过的节点作为出发节点所连接的边 进行松弛就够了

        基于以上思路,如何记录 上次松弛的时候更新过的节点呢?用队列来记录。简单理解就是每次mindist数组中被更新过的位置就是需要入栈的元素,然后栈顶元素进行松弛操作出栈,同时被修改的元素入栈,最后完成所有的循环(其实用栈也行,对元素顺序没有要求)

效率分析

        如果图越稠密,则 SPFA的效率越接近与 Bellman_ford。反之,图越稀疏,SPFA的效率就越高。一般来说,SPFA 的时间复杂度为 O(K * N) K 为不定值,因为 节点需要计入几次队列取决于 图的稠密度。

        SPFA(队列优化版Bellman_ford) 在理论上 时间复杂度更胜一筹,但实际上,也要看图的稠密程度,如果 图很大且非常稠密的情况下,虽然 SPFA的时间复杂度接近Bellman_ford,但实际时间消耗 可能是 SPFA耗时更多。

代码实现

        最根本的理解,我们这个代码是以边为主的,注意本题没有 负权回路 。

import collections

def main():
    n, m = map(int, input().strip().split())
    edges = [[] for _ in range(n + 1)]
    for _ in range(m):
        src, dest, weight = map(int, input().strip().split())
        edges[src].append([dest, weight])
    
    minDist = [float("inf")] * (n + 1)
    minDist[1] = 0
    que = collections.deque([1])
    visited = [False] * (n + 1)
    visited[1] = True
    
    while que:
        cur = que.popleft()
        visited[cur] = False
        for dest, weight in edges[cur]:
            if minDist[cur] != float("inf") and minDist[cur] + weight < minDist[dest]:
                minDist[dest] = minDist[cur] + weight
                if visited[dest] == False:
                    que.append(dest)
                    visited[dest] = True
    
    if minDist[-1] == float("inf"):
        return "unconnected"
    return minDist[-1]

if __name__ == "__main__":
    print(main())

Bellman_ford 判断负权回路

--------------------------------------------------------------------------------

        8.24更新:核心思路就是多松弛一次,看mindist数组有没有变化(负权回路会导致mindist数组变化),优化算法中的判断负权回路:如果节点加入队列的次数 超过了 n-1次 ,那么该图就一定有负权回路。

        怎么判断数组有没有变化?还是一样的逻辑,看有没有变小即可

        最后一点,普通方法把边都遍历一遍这种,没有什么特别的说法,直接多松弛一次即可,但是优化算法中的负权回路,因为引入了队列,所以一定记得对于队列内已经进入的元素不要重复进入,(这个是上一题目中的基本内容了),这里的拓展我认为有一些主动写错来纠偏的部分了,反而可能更加容易引起误导

--------------------------------------------------------------------------------

        本题是要我们判断 负权回路,也就是图中出现环且环上的边总权值为负数。如果在这样的图中求最短路的话, 就会在这个环里无限循环 (也是负数+负数 只会越来越小),无法求出最短路径。

        在上期博客中,bellman_ford 算法的核心就是一句话:对 所有边 进行 n-1 次松弛。 在没有负权回路的图中,松弛 n 次以上 ,结果不会有变化。但本题有 负权回路,如果松弛 n 次,结果就会有变化了,因为 有负权回路 就是可以无限最短路径(一直绕圈,就可以一直得到无限小的最短距离)。

        那么解决本题的 核心思路,就是在 n-1 次松弛 的基础上,再多松弛一次,看minDist数组 是否发生变化。

Bellman-Ford方法

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])

    start = 1  # 起点
    end = n    # 终点

    minDist = [float('inf')] * (n + 1)
    minDist[start] = 0
    flag = False

    for i in range(1, n + 1):  # 这里我们松弛n次,最后一次判断负权回路
        for side in grid:
            from_node = side[0]
            to = side[1]
            price = side[2]
            if i < n:
                if minDist[from_node] != float('inf') and minDist[to] > minDist[from_node] + price:
                    minDist[to] = minDist[from_node] + price
            else:  # 多加一次松弛判断负权回路
                if minDist[from_node] != float('inf') and minDist[to] > minDist[from_node] + price:
                    flag = True

    if flag:
        print("circle")
    elif minDist[end] == float('inf'):
        print("unconnected")
    else:
        print(minDist[end])

if __name__ == "__main__":
    main()

SPFA方法

        这里涉及的问题是节点重复入队,导致本应n-1次结束的过程,没有按时结束,我们需要记录哪些节点已经出队列了,哪些节点在队列里面,对于还在队列内部的节点不要重复入队

from collections import deque
from math import inf

def main():
    n, m = [int(i) for i in input().split()]
    graph = [[] for _ in range(n+1)]
    min_dist = [inf for _ in range(n+1)]
    count = [0 for _ in range(n+1)]  # 记录节点加入队列的次数
    for _ in range(m):
        s, t, v = [int(i) for i in input().split()]
        graph[s].append([t, v])
        
    min_dist[1] = 0  # 初始化
    count[1] = 1
    d = deque([1])
    flag = False
    
    while d:  # 主循环
        cur_node = d.popleft()
        for next_node, val in graph[cur_node]:
            if min_dist[next_node] > min_dist[cur_node] + val:
                min_dist[next_node] = min_dist[cur_node] + val
                count[next_node] += 1
                if next_node not in d: # 二刷的时候再好好理解
                    d.append(next_node)
                if count[next_node] == n:  # 如果某个点松弛了n次,说明有负回路
                    flag = True
        if flag:
            break
            
    if flag:
        print("circle")
    else:
        if min_dist[-1] == inf:
            print("unconnected")
        else:
            print(min_dist[-1])


if __name__ == "__main__":
    main()

Bellman_ford 单源有限最短路

--------------------------------------------------------------------------------

        8.24更新:节点数量为n,起点到终点,最多是 n-1 条边相连。 那么对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。这个n - 1要理解,这也是松弛的最基本逻辑,就不多讲了。(如果对以上讲解看不懂,建议详看 kama94.城市间货物运输I )

        但是仅有这个理解是不够的,这里我还是要吐槽一下,可能是我没看视频,我认为这道题目的文字版解析说的不好,可能是我没有理解到位,我认为出现问题的本质是:在修改过程中修改了已经遍历过的节点的值,地基变了,自然下一轮中所有的值都会修改,引起这个问题的本质是:路径中存在负权回路,而不是文字版讲解中说的:基于了本次松弛的 minDist数值,而不是上一次 松弛时候minDist的数值。这个讲解歪曲了这个问题的本质,反而让全文显得很啰嗦并且具有误导性。

        解决这个问题的方法是:解决负权环路的判断,如何解决?环路最少有两条边,隔开这两条边,也就是文字版解析中给出的解决方法,但是这个是后验的,不可以在开始就把问题归结到这一点上

        最后dijkstra显然是不能用的

--------------------------------------------------------------------------------

        注意题目中描述是 最多经过 k 个城市的条件下,而不是一定经过k个城市,也可以经过的城市数量比k小,但要最短的路径。对于部分路径节点多但是距离短的结果增加了要求

        在 这个系列的第一个题目 中我们讲了:对所有边松弛一次,相当于计算 起点到达 与起点一条边相连的节点 的最短距离。节点数量为n,起点到终点,最多是 n-1 条边相连。 那么对所有边松弛 n-1 次 就一定能得到 起点到达 终点的最短距离。本题是最多经过 k 个城市, 那么是 k + 1条边相连的节点。 

Bellman-Ford方法求解单源有限最短路

def main():
    # 輸入
    n, m = map(int, input().split())
    edges = list()
    for _ in range(m):
        edges.append(list(map(int, input().split() )))
    
    start, end, k = map(int, input().split())
    min_dist = [float('inf') for _ in range(n + 1)]
    min_dist[start] = 0
    
    # 只能經過k個城市,所以從起始點到中間有(k + 1)個邊連接
    # 需要鬆弛(k + 1)次
    
    for _ in range(k + 1):
        update = False
        min_dist_copy = min_dist.copy()
        for src, desc, w in edges:
            if (min_dist_copy[src] != float('inf') and 
            min_dist_copy[src] + w < min_dist[desc]):
                min_dist[desc] = min_dist_copy[src] + w
                update = True
        if not update:
            break
    # 輸出
    if min_dist[end] == float('inf'):
        print('unreachable')
    else:
        print(min_dist[end])
            
            

if __name__ == "__main__":
    main()

SPFA方法求解单源有限最短路

from collections import deque
from math import inf


def main():
    n, m = [int(i) for i in input().split()]
    graph = [[] for _ in range(n+1)]
    for _ in range(m):
        v1, v2, val = [int(i) for i in input().split()]
        graph[v1].append([v2, val])
    src, dst, k = [int(i) for i in input().split()]
    min_dist = [inf for _ in range(n+1)]
    min_dist[src] = 0  # 初始化起点的距离
    que = deque([src])

    while k != -1 and que:
        visited = [False for _ in range(n+1)]  # 用于保证每次松弛时一个节点最多加入队列一次
        que_size = len(que)
        temp_dist = min_dist.copy()  # 用于记录上一次遍历的结果
        for _ in range(que_size):
            cur_node = que.popleft()
            for next_node, val in graph[cur_node]:
                if min_dist[next_node] > temp_dist[cur_node] + val:
                    min_dist[next_node] = temp_dist[cur_node] + val
                    if not visited[next_node]:
                        que.append(next_node)
                        visited[next_node] = True
        k -= 1

    if min_dist[dst] == inf:
        print("unreachable")
    else:
        print(min_dist[dst])


if __name__ == "__main__":
    main()

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值