spfa算法(AcWing 851. spfa求最短路)(带图讲解,超详细)

前言

   上一篇博客中讲解了最短路问题中单源最短路中存在负权边的Bellman-Ford算法,时间复杂度为 O ( n m ) O(nm) O(nm)。(单源最短路:从1号点到n号点的最短距离)
在这里插入图片描述
   上面这张图讲贯穿我们图论中的最短路问题,大家一定要牢记,这其中的每个算法我都会进行讲解,并放到单独的一个专栏里。

   本篇博客就来讲解SPFA算法,它是Bellman-Ford算法的优化,在99%的存在负权边的问题里都可以用SPFA来进行求解(大家可以放心大胆的用,除了存在负权回路的问题,这个负权回路在下面会讲)。它可以把时间复杂度优化到 O ( m ) O(m) O(m)。由于SPFA算法是由Bellman-Ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O ( n m ) O(nm) O(nm)

题目

题目链接:851. spfa求最短路

给定一个 n n n 个点 m m m 条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1 号点到 n n n 号点的最短距离,如果无法从 1 号点走到 n n n 号点,则输出 impossible
数据保证不存在负权回路。

输入格式
第一行包含整数 n n n m m m
接下来 m m m 行每行包含三个整数 x , y , z x,y,z x,y,z,表示存在一条从点 x x x 到点 y y y 的有向边,边长为 z z z

输出格式
输出一个整数,表示 1 号点到 n n n 号点的最短距离。
如果路径不存在,则输出 impossible

数据范围
1 ≤ n , m ≤ 1 0 5 1≤n,m≤10^5 1n,m105,
图中涉及边长绝对值均不超过 10000 10000 10000

输入样例:

3 3
1 2 5
2 3 -3
1 3 4

输出样例:

2

SPFA算法

   SPFA算法仅仅只是对Bellman-Ford算法算法的一个优化。

   Bellman_ford算法会遍历所有的边,但是有很多的边遍历了其实没有什么意义,我们只用遍历那些到源点距离变小的点所连接的边即可,只有当一个点的前驱结点更新了,该节点才会得到更新;因此考虑到这一点,我们将创建一个队列每一次加入距离被更新的结点。

SPFA算法思路

明确一下松弛的概念:

  • 考虑节点u以及它的邻居v,从起点跑到v有好多跑法,有的跑法经过u,有的不经过。

  • 经过u的跑法的距离就是distu+u到v的距离。

  • 所谓松弛操作,就是看一看distv和distu+u到v的距离哪个大一点。

  • 如果前者大一点,就说明当前的不是最短路,就要赋值为后者,这就叫做松弛。

SPFA算法文字说明:

  • 建立一个队列,初始时队列里只有起始点。

  • 再建立一个数组记录起始点到所有点的最短路径(该表格的初始值要赋为极大值,该点到他本身的路径赋为0)。

  • 再建立一个数组,标记点是否在队列中。

  • 队头不断出队,计算始点起点经过队头到其他点的距离是否变短,如果变短且该点不在队列中,则把该点加入到队尾。

  • 重复执行直到队列为空。

  • 在保存最短路径的数组中,就得到了最短路径。

SPFA图解:

  • 给定一个有向图,如下,求A~E的最短路。

在这里插入图片描述

  • 源点A首先入队,然后A出队,计算出到BC的距离会变短,更新距离数组,BC没在队列中,BC入队
    在这里插入图片描述
  • B出队,计算出到D的距离变短,更新距离数组,D没在队列中,D入队。然后C出队,无点可更新。
    在这里插入图片描述
  • D出队,计算出到E的距离变短,更新距离数组,E没在队列中,E入队。

在这里插入图片描述

  • E出队,此时队列为空,源点到所有点的最短路已被找到,A->E的最短路即为8。

在这里插入图片描述

详细代码

这里首先放别人写的代码,网上的基本都是这种样式,构建邻接表非常繁琐并且可读性较差。

from collections import deque
# 把所有点更新完了,最短路径就出来了
N = 100010
inf = float("inf")
graph = [-1] * N
weight = [inf] * N
dist = [inf] * N
e = [0] * N
ne = [0] * N
idx = 0
state = [False] * N  # 标记节点是否在更新队列中(更新队列中不能有重复元素)

def add(a,b,c):
    global idx 
    idx += 1
    e[idx] = b
    ne[idx] = graph[a]
    graph[a] = idx
    weight[idx] = c

def spfa():
    queue = deque()
    dist[1] = 0
    queue.append(1)
    state[1] = True

    while queue:
        node = queue.popleft()
        state[node] = False

        cur = graph[node]
        while cur != -1:
            j = e[cur]
            if dist[j] > weight[cur] + dist[node]:
                dist[j] = weight[cur] + dist[node]
                if not state[j]:
                    state[j] = True
                    queue.append(j)
            cur = ne[cur]

    if dist[n] == inf:
        return "impossible"
    else:
        return dist[n]

if __name__ == "__main__":
    n,m = map(int,input().split())
    while m > 0:
        a,b,c = map(int,input().split())
        add(a,b,c)
        m -= 1

    print(spfa())

作者:Philosober
链接:https://www.acwing.com/activity/content/code/content/986481/
来源:AcWing
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

下面放下我自己写的代码,包括之前的邻接表和邻接矩阵的构建我都是这么写的,自己觉得很好理解并且读懂(对于邻接表和邻接矩阵的构建如果有疑问的可以看我之前写的博客Dijkstra求最短路篇二(全网最详细讲解两种方法,适合小白)(python,其他语言也适用)(超简单的邻接表的构建)

# 导入双端队列模块,用于实现SPFA算法中的队列
from collections import deque

# 读取输入的两个整数n和m,分别表示图中的节点数和边数
n, m = map(int, input().split())

# 初始化一个列表num,用于存储每个节点的邻接点和对应的边的权重
# 列表的大小为n+1,因为节点编号从1开始,索引从0开始,所以需要+1来匹配
num = [[] for i in range(n + 1)]

# 初始化一个列表dist,用于存储从起点(节点1)到每个节点的最短距离
# 初始值设为无穷大,表示还未计算到该节点的最短距离
dist = [float('inf')] * (n + 1)
# 起点到自身的距离为0
dist[1] = 0

# 初始化一个列表state,用于标记每个节点是否在队列中
# 在SPFA算法中,我们需要避免重复处理队列中的节点
state = [False] * (n + 1)

# 读取m条边的信息,每条边由三个整数a, b, w组成,表示从节点a到节点b有一条权重为w的边
for i in range(m):
    a, b, w = map(int, input().split())
    num[a].append((b, w))

# 定义SPFA算法的函数
def spfa():
    # 创建一个双端队列q,用于存储待处理的节点
    q = deque()
    # 将起点(节点1)加入队列
    q.append(1)
    
    # 当队列不为空时,持续处理队列中的节点
    while q:
        # 从队列的左端取出一个节点a
        a = q.popleft()
        # 将节点a标记为不在队列中
        state[a] = False
        
        # 遍历节点a的所有邻接点b和对应的边的权重w
        for b, w in num[a]:
            # 如果通过节点a到达节点b的路径比当前已知的最短路径更短
            if dist[b] > dist[a] + w:
                # 更新节点b的最短路径
                dist[b] = dist[a] + w
                # 如果节点b不在队列中,则将其加入队列,并标记为在队列中
                if not state[b]:
                    q.append(b)
                    state[b] = True
    
    # 如果无法到达终点(节点n),则最短路径不可能存在
    if dist[n] == float('inf'):
        return 'impossible'
    else:
        # 返回从起点到终点的最短路径长度
        return dist[n]

# 调用SPFA算法函数,并打印结果
print(spfa())

值得注意的是

  1. ⭐️ state数组的作用:判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到源点的距离,那只用更新一下数值而不用加入到队列当中。
    即便不使用state数组最终也没有什么关系,但是使用的好处在于可以提升效率。

  2. ⭐️SPFA算法看上去和Dijstra算法长得有一些像但是其中的意义还是相差甚远的:

   Dijkstra算法中的state数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为True后改变为False);SPFA算法中的state数组仅仅只是表示的当前发生过更新的点,且SPFA中的state数组可逆(可以在标记为True之后又标记为False)。顺带一提的是BFS中的state数组记录的是当前已经被遍历过的点。
   
    Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。

  1. ⭐️ Bellman-Ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
  2. ⭐️由于SPFA算法是由Bellman-Ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O ( n m ) O(nm) O(nm) ,假如题目时间允许可以直接用SPFA算法去解Dijkstra算法的题目。
  3. ⭐️判断是否存在负环一般使用SPFA算法,方法是用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。(下一篇博客中就是这种问题)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

不染_是非

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值