SPFA算法详解——判断负权环

article/2025/10/23 15:42:22

  • 算法核心
  • 代码实现
  • 负权环
  • 判断负权环
    • 根据松弛次数
    • 根据最短路径上的死循环

SPFA(Shortest Path Faster Algorithm)(队列优化)算法是求单源最短路径的一种算法。它是在Bellman-ford算法的基础上加上一个队列优化,减少了冗余的松弛操作,是一种高效的最短路算法。
Bellman-Ford算法虽然可以处理负环,但是时间复杂度为O(ne),e为图的边数,在图为稠密图的时候,是不可接受的。
Bellman-Ford算法的缺点在于,当某一个迭代求解已经获得了所有的最短路径后,它还是会继续执行没有执行完的迭代求解。但其实不用这样。
分析不难发现,起点s到某一个点的最短路径的第一条边,必定是s与s的邻接点连成的边。所以当我们在第一次松弛(即第一次迭代求解时)时,松弛的边必定包含最短路径的第一条边。
而最短路径的第二条边,必定是s的邻接点与s的邻接点的邻接点连成的边。这样以此类推。

算法核心

因此,可以对算法进行优化。设置一个队列,初始的时候将s放入队列中。
【1】队列出队,出队元素为current,松弛current与其邻接点相连的边,将松弛成功的邻接点放入队列中,这些点中包含其最短路径的第二个点(第一个点为起点)
【2】然后再次队列出队,出队元素为current,松弛current与其邻接点相连的边,但如果已在队列中就不要重复入队了
【3】重复以上步骤
其实这个步骤和无权最短路径算法有点像。

代码实现

这里写图片描述

from queue import Queue
class Edge():def __init__(self,u,v,cost):self.u = uself.v = vself.cost = cost
nodenum = 5edgeList = []
dis = [float("inf")]*nodenum
pre = [-1]*nodenum
know = [False]*nodenum#代表已在队列之中edgeList.append(Edge(1,4,2))
edgeList.append(Edge(0,2,4))
edgeList.append(Edge(3,2,5))
edgeList.append(Edge(3,1,1))
edgeList.append(Edge(1,3,2))
edgeList.append(Edge(4,3,-3))
edgeList.append(Edge(0,1,-1))
edgeList.append(Edge(1,2,3))edgenum = len(edgeList)original = 0
def SPFA(original):q = Queue()dis[original] = 0know[original] = Trueq.put(original)while(not q.empty()):current = q.get()know[current] = False#循环遍历current的邻接顶点for edge in edgeList:if(edge.u == current):#current点的邻接边temp = dis[edge.u] + edge.costif(temp < dis[edge.v]):#松弛操作dis[edge.v] = temppre[edge.v] = currentif(not know[edge.v]):q.put(edge.v)know[edge.v] = Trueprint('当前出队的元素为',current)print(dis)print(pre,'\n')SPFA(original)
print('\n',dis)
print(pre)

这里写图片描述
从运行结果可以看出,程序的执行过程。2出队了两次,说明它也入队了两次。从上到下观察dis数组,可以发现每个点的dis最多被更新两次,有的更新一次就更好好了。

负权环

当图中有负权环时,队列就不会有空的情况了,因为由于负权环的存在,负权环上的点就可以一直被松弛,一直能被松弛就代表着队列会不断反复让负权环上的点入队出队,程序就会死循环。
修改边的权重为:

edgeList.append(Edge(0,1,-1))
edgeList.append(Edge(1,2,3))
edgeList.append(Edge(3,1,1))
edgeList.append(Edge(1,3,2))
edgeList.append(Edge(1,4,2))
edgeList.append(Edge(0,2,4))
#edgeList.append(Edge(3,2,5))
edgeList.append(Edge(2,3,-5))
edgeList.append(Edge(4,3,-3))


如图所示,形成了123的负权环。
此时,运行结果会无限输出,不停有元素出队入队,所以程序陷入了死循环。

判断负权环

根据松弛次数

n代表节点个数。
根据松弛次数是否大于等于n来判断负权环,是从网上其他博客说的,根据出队次数是否大于等于n来判断,想到的。因为,判断出队次数,是判断更新次数的上界。
用一个n大小的数组来代表每个点的松弛次数。因为SPFA算法里的松弛,和Bellman-ford算法里的松弛一样。Bellman-ford算法里,对同一个点的松弛次数,在极端情况下,可以想象把这些松弛次数分配到每一次迭代求解中去,而迭代求解一共只有n-1次。所以一旦某个点的松弛次数等于了n,那么就说明有负环。
所以,在每次进行松弛后,遍历判断每个点的松弛次数,如果有一个等于n(再执行下去就会大于n),就说明有负环。

#用松弛次数来判断负权环
from queue import Queue
class Edge():def __init__(self,u,v,cost):self.u = uself.v = vself.cost = cost
nodenum = 5edgeList = []
dis = [float("inf")]*nodenum
pre = [-1]*nodenum
know = [False]*nodenum#代表已在队列之中update = [0]*nodenum#代表每个点被更新的次数edgeList.append(Edge(0,1,-1))
edgeList.append(Edge(1,2,3))
edgeList.append(Edge(3,1,1))
edgeList.append(Edge(1,3,2))
edgeList.append(Edge(1,4,2))
edgeList.append(Edge(0,2,4))
#edgeList.append(Edge(3,2,5))
edgeList.append(Edge(2,3,-5))
edgeList.append(Edge(4,3,-3))edgenum = len(edgeList)original = 0
def SPFA(original):q = Queue()dis[original] = 0know[original] = Trueq.put(original)while(not q.empty()):current = q.get()know[current] = Falseflag = False#负环判断标记#循环遍历current的邻接顶点for edge in edgeList:if(edge.u == current):#current点的邻接边temp = dis[edge.u] + edge.costif(temp < dis[edge.v]):#松弛操作dis[edge.v] = temppre[edge.v] = currentupdate[edge.v] += 1for i in update:if(i==nodenum):flag =Trueprint('最后一次出队的是',current)breakif(flag == True):breakif(not know[edge.v]):q.put(edge.v)know[edge.v] = Trueif(flag == True):breakprint('当前出队的元素为',current)print(dis)print(pre,'\n')SPFA(original)
print('dis',dis)
print('pre',pre)
print('update',update)

这里写图片描述
运行结果并没有全部截图下来,因为中间执行了很多次不该有的出队入队操作(每次出队都输出东西),这里只截了最后结果。可以看到update数组中,有一个等于了5,所以程序就判断有了负权环。
而且读者可以通过所有的输出结果,统计每个点出队次数,会发现,这些点里面出队次数最大也就为4(我用画正字来记的数==)。意思就是,如果程序通过出队次数来判断的话,那么程序还得多执行几次,不该有的出队入队操作。这也证实了,判断出队次数,是判断更新次数的上界。
但通过所有输出结果,还是觉得程序判断出负权环判断得太迟了(中间执行了很多次不该有的出队入队操作),有没有一种更快的方法,可以及早判断出负权环呢。答案是有,如下。

根据最短路径上的死循环

寻找最短路径的方法是通过pre数组:
比如代码实现章节的程序运行后,pre数组为[-1, 0, 1, 4, 1],要寻找从起点0到3的最短路径,步骤如下:
【1】记录下3,路径为=>3
【2】记录下pre[3]=4,路径为=>4=>3
【3】记录下pre[4]=1,路径为=>1=>4=>3
【4】记录下pre[1]=0,路径为=>0=>1=>4=>3
【4】记录下pre[0]=-1,遇到-1,到达终点,返回路径=>0=>1=>4=>3
从起点到某点的最短路径,路径上的点必定都是不重复的。但当有负权环时,这句话就不成立了。
比如负权环章节的程序运行后,pre数组为[-1, 3, 1, 2, 1],寻找从起点0到3的最短路径,会发现,上述步骤会一种进行下去,因为到达不了终点,死循环了(虽然这里程序是用的递归)。

#此程序用pre数组的死循环来判断是否有负环
from queue import Queue
class Edge():def __init__(self,u,v,cost):self.u = uself.v = vself.cost = cost
nodenum = 5edgeList = []
dis = [float("inf")]*nodenum
pre = [-1]*nodenum
know = [False]*nodenum#代表已在队列之中edgeList.append(Edge(0,1,-1))
edgeList.append(Edge(1,2,3))
edgeList.append(Edge(3,1,1))
edgeList.append(Edge(1,3,2))
edgeList.append(Edge(1,4,2))
edgeList.append(Edge(0,2,4))
#edgeList.append(Edge(3,2,5))
edgeList.append(Edge(2,3,-5))
edgeList.append(Edge(4,3,-3))edgenum = len(edgeList)original = 0def if_circle(pre):#判断是否有负权环,返回真假,如有负权环,并返回环prev = -1#设置环的起点circle = []#记录负权环def get(i):circle.append(i)if(i == -1):#到达了正常的终点,判断无负权环return Falseif(i == prev):#到达了不该达到的终点,判断有负权环return Truereturn get(pre[i])for i in pre:if(i == -1):#超出索引限制了continueprev = iif(get(pre[i])):#传入参数直接是i的上一个顶点,直接传入i会出错return (True,circle)circle.clear()return (False,)def SPFA(original):q = Queue()dis[original] = 0know[original] = Trueq.put(original)while(not q.empty()):current = q.get()know[current] = Falseflag = False#循环遍历current的邻接顶点for edge in edgeList:if(edge.u == current):#current点的邻接边temp = dis[edge.u] + edge.costif(temp < dis[edge.v]):#松弛操作dis[edge.v] = temppre[edge.v] = currentif(if_circle(pre)[0]):print('有负环为',if_circle(pre)[1])print(dis)print(pre)flag = Truebreakif(not know[edge.v]):q.put(edge.v)know[edge.v] = Trueif(flag == True):breakprint('当前出队的元素为',current)print(dis)print(pre,'\n')SPFA(original)
print('\n',dis)
print(pre)

这里写图片描述
通过这种方式,程序可以很快地判断出来负权环(程序只出队了4次就判断出来了)。且得到了负权环的组成的点[2, 1, 3]。


http://chatgpt.dhexx.cn/article/bNqZwioD.shtml

相关文章

(算法基础)SPFA算法

适用情景 SPFA算法一般运用在求最短路问题当中的单源最短路中存在负权边的情况&#xff08;正权边如果出题人没有特别恶心去卡的话&#xff0c;也是ok的&#xff09;&#xff0c;SPFA算法还可以去判断图中是否存在负环。 时间复杂度 一般来讲是O(m)&#xff0c;m就是图当中的边…

透析SPFA算法(图例讲解)

SPFA算法是Bellman-Ford的队列优化&#xff0c;所以先介绍Bellman-Ford算法。 Dijkstra算法是处理单源最短路径的有效算法&#xff0c;但它局限于边的权值非负的情况&#xff0c;若图中出现权值为负的边&#xff0c;Dijkstra算法就会失效&#xff0c;求出的最短路径就可能是错的…

SPFA 算法详解( 强大图解,不会都难!)

适用范围&#xff1a;给定的图存在负权边&#xff0c;这时类似Dijkstra等算法便没有了用武之地&#xff0c;而Bellman-Ford算法的复杂度又过高&#xff0c;SPFA算法便派上用场了。 我们约定有向加权图G不存在负权回路&#xff0c;即最短路径一定存在。当然&#xff0c;我们可以…

OSPF NSSA区域路由的计算过程与FA值实验

概述&#xff1a; NSSA区域中不存在5类LSA&#xff0c;但是为了描述区域外的路由条目&#xff0c;则用7类LSA代替5类LSA。 7类LSA仅在NSSA区域中存在&#xff0c;当7类LSA离开NSSA区域进入普通区域时&#xff0c;可以转化为5类LSA。 NSSA-LSA报文格式和5类LSA相同&#xff1…

求不规则立方体表面积java_不规则立方体体积计算

展开全部 计算不规则的立体图形体积可62616964757a686964616fe78988e69d8331333431373866以把这个物体放入水中&#xff0c;用现在容积-未放入物体的容积就是体积或用放入物体后高-未放入物体*长*宽(1升1立方分米&#xff1b;1毫升1立方厘米)。 体积的国际单位制是立方米。一件…

冠层分析法(VCP)提取叶面积指数

文章目录 一、简介二、代码实现三、小结参考文献一、简介 该方法最早是由Omasa等人提出,是根据激光雷达光束与树木的接触频率来计算光束截获概率。总结来讲具体过程分为三步: 1、 从三维体素模型网格里判断是否有点,有点的网格标记为1,没点的网格标记为0,没点的网格表明冠…

SPFA例题

引入 SPFA的核心思想是优化dist[a]min(dist[a],dist[b]w)这个式子&#xff1b; 不难想到&#xff0c;如果我们想要优化dist[a]&#xff0c;那么只能从dist[b]w这个式子转移过来&#xff1b; 也就是说&#xff0c;只有dist[b]变小了&#xff0c;才有可能优化dist[a]&#xff…

OSPF中的SPF计算

OSPF域内路由计算&#xff1a;SPF计算 在1类LSA和2类LSA中&#xff0c;包括了拓扑信息和路由信息 Phase 1 &#xff1a;根据1类LSA中的P2P、TransNet、2类LSA&#xff0c;构建SPF树 Phase 2&#xff1a;根据1类LSA中的Stub、2类LSA&#xff0c;计算最优路由 左边是拓扑图&am…

聊一聊有限体积法

导读&#xff1a;有限体积法的工作原理。 一、求解对象 首先提一下我们的主角Navier-Stokes方程&#xff1a; 为了能够通过SIMPLE算法求解Navier-Stokes方程&#xff0c;我们需要将该方程改写为下列矩阵形式的方程&#xff1a; 其中为了系数矩阵&#xff0c;是每个网格未知…

IDEA中新导入的项目找不到maven project解决办法

刚导入的项目&#xff0c;导入进来之后&#xff0c;找不到maven project&#xff0c;idea打开view ->tool windows下也没有maven模块。查看了setting下plugins中maven插件也在。最后发现项目在导入过程中&#xff0c;因为选择的原因&#xff0c;没有指定位maven工程&#xf…

win10删除文件提示'找不到该项目'

win10系统下删除文件时出现了"找不到该项目"问题 遇到一个问题: win10系统下删除文件时出现了"找不到该项目"问题 百度搜索后得到 DEL /F /A /Q \\?\%1 RD /S /Q \\?\%1 这两行dos命令 创建(.txt)文件后写入这两行命令,保存,修改(.txt)至(.bat) 最后…

IDEA项目下不显示target目录或者target目录不完整没有新添加的资源

1.首先解决不显示target目录的问题 需要知道:如果不是maven工程,是没有target目录的,其次编译后才会生成target目录. 普通项目会生成out目录 a>如果设置过隐藏target目录 只需要找到settings-->Editor-->File Type-->Ignore Files and Folder删除该项 b>如果…

项目启动报错,找不到文件

java.io.FileNotFoundExceptionE:\software\apache-maven-3.6.1-bin\apache-maven-3.6.1\localrepository\xalan\xalan\2.7.0\xercesImpl.jar (系统找不到指定的文件。) java.io.FileNotFoundExceptionE:\software\apache-maven-3.6.1-bin\apache-maven-3.6.1\localrepository\…

idea项目类存在却找不到

这种情况&#xff0c;我真是奇了怪了&#xff0c;服了。我试了一下&#xff0c;试出了解决方案&#xff1a;将这个类移到同级另一个包&#xff0c;看见不报错了&#xff0c;再移回去。属于给idea虚晃一枪&#xff0c;再运行…emmmmm也还是报错的。于是不断的删iml文件&#xff…

Windows 删除”找不到该项目”文件夹

问题根源&#xff1a; 使用不可显示ASCII字符或采用UNICODE字符方法创建的文件或文件夹&#xff1b; 名称中含有..等特殊符号文件或文件夹名称不符合Windows命名规范或建立空格目录名创建的文件或文件夹&#xff1b; 使用下载工具创建的文件夹,在未下载完成前自行删除文件或…

idea找不到项目目录

有时候idea拉取项目时出现没有目录&#xff0c;src.main等等&#xff0c;下面就来带大家找到项目目录并显示出来 首先我们打开文件&#xff08;在左上角——找到项目结构&#xff09; 然后我们找到模块——点击左边那个加号,然后选择自己的项目目录。再选择一下jdk版本&#x…

【常见问题】vs 找不到导入的项目,请确认import声明

问题描述 当复制人家的vs项目文件到自己电脑上进行调试&#xff0c;重新生成解决方案失败&#xff0c;并报如下错误。 解决方法 1.分析报错内容&#xff0c;出现问题文件为vcxproj文件。 2.找到vcxproj文件&#xff0c;用记事本打开。 3.CTRLF搜索“Import”&#xff0c…

无法删除提示找不到该项目,该项目不在C:\users\?win10无法强力删除文件?

无法删除提示找不到该项目,该项目不在C:\users\?win10无法强力删除文件? 新建个TXT &#xff0c;把下边的代码粘贴过去&#xff1a; DEL /F /A /Q \\?\%1 RD /S /Q \\?\%1 保存之后把名字改成&#xff1a;Delete.bat 然后把要删除的文件夹拖到这个文件上&#xff0c;即可删…

git拉取项目报错:找不到项目 Repository not found

这段时间换了新公司&#xff0c;不想每天背着电脑跑来跑去了&#xff0c;就用了公司配发的电脑&#xff0c;前两天想用Git拉取公司的项目的时候却碰见问题了&#xff0c;无论是Git命令行拉取还是使用idea拉取&#xff0c;都会报错找不到项目 但是在浏览器使用链接是可以访问到项…

问题:找不到该项目,该项目不在指定目录下.......请确认位置......

参考网址&#xff1a;http://bbs.kafan.cn/thread-1398433-1-1.html 1.把下面的代码复制粘贴到一新建的txt记事本文档中&#xff0c;并另存为del.bat文件&#xff08;或者你喜欢的名字&#xff09;&#xff0c;注意扩展名为批处理文件bat&#xff1b; DEL /F /A /Q \\?\%1 RD …