前言
上一篇博客中讲解了最短路问题中单源最短路中存在负权边的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
1≤n,m≤105,
图中涉及边长绝对值均不超过
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())
值得注意的是
-
⭐️ state数组的作用:判断当前的点是否已经加入到队列当中了;已经加入队列的结点就不需要反复的把该点加入到队列中了,就算此次还是会更新到源点的距离,那只用更新一下数值而不用加入到队列当中。
即便不使用state数组最终也没有什么关系,但是使用的好处在于可以提升效率。 -
⭐️SPFA算法看上去和Dijstra算法长得有一些像但是其中的意义还是相差甚远的:
Dijkstra算法中的state数组保存的是当前确定了到源点距离最小的点,且一旦确定了最小那么就不可逆了(不可标记为True后改变为False);SPFA算法中的state数组仅仅只是表示的当前发生过更新的点,且SPFA中的state数组可逆(可以在标记为True之后又标记为False)。顺带一提的是BFS中的state数组记录的是当前已经被遍历过的点。
Dijkstra算法里使用的是优先队列保存的是当前未确定最小距离的点,目的是快速的取出当前到源点距离最小的点;SPFA算法中使用的是队列(你也可以使用别的数据结构),目的只是记录一下当前发生过更新的点。
- ⭐️ Bellman-Ford算法可以存在负权回路,是因为其循环的次数是有限制的因此最终不会发生死循环;但是SPFA算法不可以,由于用了队列来存储,只要发生了更新就会不断的入队,因此假如有负权回路请你不要用SPFA否则会死循环。
- ⭐️由于SPFA算法是由Bellman-Ford算法优化而来,在最坏的情况下时间复杂度和它一样即时间复杂度为 O ( n m ) O(nm) O(nm) ,假如题目时间允许可以直接用SPFA算法去解Dijkstra算法的题目。
- ⭐️判断是否存在负环一般使用SPFA算法,方法是用一个cnt数组记录每个点到源点的边数,一个点被更新一次就+1,一旦有点的边数达到了n那就证明存在了负环。(下一篇博客中就是这种问题)