当前位置: 首页 > ops >正文

94. 城市间货物运输 I, Bellman_ford 算法, Bellman_ford 队列优化算法

94. 城市间货物运输 I

Bellman_ford 算法

Bellman_ford 算法 与 dijkstra算法 相比通用性更强。

  dijkstra算法解决不了负权边的问题,因为Dijkstra基于贪心策略一旦一个节点被从队列中取出(标记为已解决),它就假定已经找到了到达该节点的最短路径。如果存在负权边,可能会有一条通过负权边的路径去到达该节点,这个负权边可以让一个已经被标记为已解决的节点的距离变得更小,但算法不会再回头去检查它,从而导致错误的结果。

  Bellman_ford 算法的核心思想是 对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路

什么是松弛?

松弛是Bellman_ford 算法的实现基础。

松弛就是一个“检查并更新”的过程:检查是否存在一条通过某个中间节点的更短路径,如果存在,就更新当前的最短路径估计。其实就是更新min_List数组,来判断从起点出发到达下一个点是否有更短的路径。

代码实现:

# 对边 (u, v) 进行松弛操作
if dist[u] + weight(u, v) < dist[v]:dist[v] = dist[u] + weight(u, v)prev[v] = u # 可选:记录v是从u来的

为什么是n-1次?

因为任何不包含负权环的最短路径最多包含 n-1 条边。n-1 次松弛足以保证找到所有可能的最短路径。 即n个点的话,从起点出发到终点的最短路径只需要经过n-1条边。

Bellman_ford 算法为什么能解决负权边的问题?

为什么dijkstra算法不能解决负权边的问题,关键是visited数组的设置导致了即使有负权边通向某个结节点,但由于visited中该节点之前被访问过了,因此无法更新。

Bellman-Ford 算法不依靠visited数组,其实现就是从每个点出发去松弛n-1次,来得到从起点出发经过该节点到达下一个节点时的最短路径。如下

dist 初始化为 [ ∞, 0, ∞, ∞, ∞, ∞ ],n=5,因此需要松弛4次。

第一次松弛:

  • 边 1→2 (100): dist[1] + 100 = 0 + 100 = 100 < ∞ → 更新 dist[2] = 100
  • 边 1→3 (1): dist[1] + 1 = 0 + 1 = 1 < ∞ → 更新 dist[3] = 1
  • 边 2→3 (-300): dist[2] + (-300) = 100 - 300 = -200 < 1 → 更新 dist[3] = -200
  • 边 3→4 (1): dist[3] + 1 = -200 + 1 = -199 < ∞ → 更新 dist[4] = -199
  • 边 4→5 (1): dist[4] + 1 = -199 + 1 = -198 < ∞ → 更新 dist[5] = -198

第二次松弛

  • 边 1→2 (100): 0 + 100 = 100 = 100 → 无变化
  • 边 1→3 (1): 0 + 1 = 1 > -200 → 无变化
  • 边 2→3 (-300): 100 - 300 = -200 = -200 → 无变化
  • 边 3→4 (1): -200 + 1 = -199 = -199 → 无变化
  • 边 4→5 (1): -199 + 1 = -198 = -198 → 无变化

......

经过4次松弛后就得到了更新后的min_List数组。从树的生成角度来理解这两个算法,如下:

Code

if __name__ == "__main__":city_size, road_size = map(int, input().split())length = city_size + 1graph = [[] for _ in range(length)] ##graph 存储了length个空数组min_List = [float('inf')] * lengthmin_List [1] = 0for _ in range(road_size):s, t, v = map(int, input().split())graph[s].append([t,v])for _ in range(city_size):      ## 松弛n-1次updated = False      ## 不一定要松弛完n-1次,如果发现不再更新了,那就可以退出了for i in range(1, length):       ## 遍历每个点cur_node = i       if len(graph[i]) == 0:       ## 没有下一个节点 continuefor j in range(len(graph[i])):  ## 每个节点进行了n-1次循环。即n-1次松弛next_node = graph[i][j][0]value = graph[i][j][1]dis = min_List[cur_node] + value  # 从起点出发经历当前节点到下一个节点if min_List[cur_node] != float('inf') and dis < min_List[next_node]:min_List[next_node] = disupdated = Trueif updated == False:   # 在一次松弛过程中,遍历完所有节点后发现min_List没有更新break if min_List[-1] == float('inf'):print("unconnected")else:print(min_List[-1])

注意:非常重要的点:

  1. 松弛是外循环,遍历节点在内循环。
  2. 一次松弛是对每个节点之间的边关系进行松弛,而不是一次松弛对当前节点下的所有边关系进行松弛,要搞清楚这二者的区别。

前者才是在找最优解,后者问题很大,看似像dijkstra算法,但dijkstra算法是每次选当前离起点距离最小的未访问节点去进行遍历,后者现在的问题是最短路径一定要是按编号顺序走去到吗?如下图:

正确路径应该是1-3-2-4,后者是顺序遍历节点,当遍历到3时,此时虽然能更新节点2的min_List值,但由于后续你不会再去遍历节点2了,因此找不到最优解。

Bellman_ford 队列优化算法

优化思路:

没用队列优化的Bellman_ford算法时间复杂度是O(N*E), N是松弛次数,E是节点数目。由于上述Bellman_ford算法我们通过数组来有序地存储了节点从小到大的边的关系,因此本身也算是优化了。

如果没有用数组有来序存储了节点从小到大的边的关系,而是直接根据题意提供的边的顺序进行松弛,那就会出现一个问题。当我们遍历当前节点如5时,想要松弛得到min_List[6],但当前min_List[5] 为 max,因为 1 -> 2 这条边的关系放在后面,因此还没松弛到,此时那你松弛5 -> 6这条边就是在做无用功。边的提供顺序如下:

252
56-2
531
121
135

此时,你在根据这个顺序进行松弛操作就做了很多无用功,因为你要算下一个节点到起点的最短距离,但距离起点更近的点此时候都不知道,那松弛就是在做无用功。如下,你要求5,3,6到1的最近距离,那关键边1-2, 1-3 都没传进来,此时松弛就是无用功。

Code

from collections import dequeif __name__ == "__main__":city_size, road_size = map(int, input().split())length = city_size + 1graph = [ [] for _ in range(length)]min_List = [float('inf')] * lengthmin_List [1] = 0for _ in range(road_size):s, t, v = map(int, input().split())graph[s].append([t,v])     ## 直接按提供的边的顺序进行存储queue = deque()queue.append(1)     ## 先将起点存入,queue的顺序为从起点开始到终点的经过过的节点顺序visited = [False] * lengthvisited[1] = Truefor i in range(city_size):   ## 松弛n-1次 updated = Falsewhile queue:cur_node = queue.popleft()visited[cur_node] = False       ## 方便下次松弛时,visited数组更改为False了,这样你才能对上层节点的边进行更新。如果上层节点松弛后更新了,那后续就添加到队列中继续进行松弛操作了。for t, v in graph[cur_node]:     ## 直接从上一个节点下的链表进行查找下一个点if min_List[cur_node] + v < min_List[t]:min_List[t] = min_List[cur_node] + v    ## 对边进行松弛if visited[t] == False:     ## 下一个点不在队列里面visited[t] = True       ## 对松弛后更新的边进行标记,让其只在队列中出现一次,以确保后续不重复松弛queue.append(t)     ## A -> B, 现在是从A去处理B之前的节点边关系,后面就是从B出发去处理下一个节点之前的边的节点关系updated = Trueif updated == False:        ## 松弛后发现min_List不变了,那后续没必要松弛了breakif min_List[-1] == float('inf'):print("unconnected")else:print(min_List[-1])

注意: 

  1. visited的设置是为了队列中每一个元素都是唯一的,保证不重复添加相同的元素,比如当前队列中已经有6这个节点,如果你没有visited数组,后续在松弛的过程又对6的min_list数组进行了更新,又往队列中添加了一次节点6,导致队列中出现两次节点6,后续就对节点6进行了重复的松弛的操作。visited的设置主要是为了优化,屏蔽掉的话也可以通过,但当边的关系很多很复杂时,此时可能就会存在超时的情况。
  2. 在时间复杂度上,该优化方法的时间复杂度为O(N*k),N表示节点数量,k表示每个节点的出度。
http://www.xdnf.cn/news/18463.html

相关文章:

  • 智慧工厂烟雾检测:全场景覆盖与精准防控
  • Java基础 8.22
  • 2-3.Python 编码基础 - 类型检测与类型转换
  • 集成电路学习:什么是SVM支持向量机
  • AI 大模型 “进化史”:从参数竞赛到场景落地,技术突破藏着哪些逻辑?
  • Unreal Engine UFloatingPawnMovement
  • 【ECharts】2. ECharts 性能优化
  • kafka的rebalance机制是什么
  • CentOS 10安装Ollama
  • 12-Linux系统用户管理及基础权限
  • 机试备考笔记 18/31
  • Nginx(一)认识Nginx
  • Eino 开源框架全景解析 - 以“大模型应用的搭积木指南”方式理解(一)
  • Azure TTS Importer:一键导入,将微软TTS语音接入你的阅读软件!
  • LeetCode 3195.包含所有 1 的最小矩形面积 I:简单题-求长方形四个范围
  • 【ElasticSearch】IK分词器安装,配置修改,支持新增词组,中文常用mapping使用案例
  • 微前端qiankun框架,子页面图标样式错乱问题,显示为X
  • 人脸识别驱动的工厂人体属性检测与预警机制
  • Conmi的正确答案——Ubuntu24.04禁用任何休眠
  • huggingface离线下载模型使用方法
  • CAN总线工具学习:DBC解析、设备扫描与报文监控
  • Logstash——性能、可靠性与扩展性架构
  • JAVA后端开发——API状态字段设计规范与实践
  • Claude Code接入Serena mcp
  • Elasticsearch Rails 集成(elasticsearch-model / ActiveRecord)
  • [激光原理与应用-317]:光学设计 - Solidworks - 零件、装配体、工程图
  • 浅拷贝,深拷贝
  • 【生成树+环】题解:P3907 环的异或_图论_环_异或_搜索_算法竞赛_C++
  • 【C++】多态(详解)
  • 单片机---------WIFI模块