更弱智的算法学习 day59

Dijkstra算法的堆优化版本是针对其朴素算法效率不足的改进,特别适用于稀疏图​(边数远小于顶点数平方的图)。下面我将从算法思想和代码实现两方面进行解释。

算法思想

Dijkstra算法用于求解单源最短路径问题 ,即从给定的起点出发,计算其到图中所有其他顶点的最短路径。它的核心是贪心策略,基本步骤如下:

  1. 初始化​:将起点到自身的距离设为0,到其他所有顶点的距离初始化为无穷大。

  2. 选择未访问最近节点​:从所有尚未确定最短路径的顶点中,选出当前距离起点最近的一个。

  3. 松弛操作​:对于刚选出的这个顶点的所有邻居,检查是否存在一条通过当前顶点到达该邻居的更短路径。如果有,则更新该邻居顶点的最短距离。

  4. 标记已访问​:将当前顶点标记为已处理,其最短距离已确定。

  5. 重复​:重复步骤2-4,直到所有顶点都被处理完毕。

在朴素Dijkstra算法中,步骤2"选择未访问最近节点"是通过遍历所有未访问节点来实现的,这使得其时间复杂度为O(n²)。当顶点数量n很大时,效率较低。

堆优化的核心思想 在于使用一个最小堆(优先队列)​​ 来维护所有当前"距离起点距离已知但尚未最终确定"的顶点。堆顶元素始终是当前距离最小的顶点。这样,步骤2中获取最小距离节点的操作时间复杂度可以从O(n)降低到O(1)(获取堆顶),而每次调整堆(插入或删除)的时间复杂度为O(log n),从而将算法的整体时间复杂度优化到O((n + m) log n),其中m是边的数量。

代码实现解析

python 复制代码
import heapq

class Edge:
    def __init__(self, to, val):
        self.to = to    # 这条边指向的顶点
        self.val = val  # 边的权重

def dijkstra(n, m, edges, start, end):
    # 1. 图的构建:使用邻接表
    grid = [[] for _ in range(n + 1)]
    for p1, p2, val in edges:
        grid[p1].append(Edge(p2, val)) # 存储从p1出发到p2的边

    # 2. 初始化数据结构
    minDist = [float('inf')] * (n + 1) # 记录起点到每个顶点的最短距离估计值
    visited = [False] * (n + 1)        # 标记顶点是否已确定最短距离
    pq = []                            # 最小堆(优先队列)

    # 起点初始化
    minDist[start] = 0
    heapq.heappush(pq, (0, start)) # 将起点(距离0, 顶点编号)入堆

    # 3. 主循环
    while pq:
        # 3.1 取出当前距离起点最近的未访问顶点
        cur_dist, cur_node = heapq.heappop(pq)

        # 关键优化:如果该顶点已被处理过,则跳过(由于堆中可能存在同一顶点的多个历史距离)
        if visited[cur_node]:
            continue

        # 标记当前顶点为已访问,此时cur_dist就是它的最终最短距离
        visited[cur_node] = True

        # 3.2 松弛操作:遍历当前顶点的所有邻居
        for edge in grid[cur_node]:
            # 如果邻居未被访问,且通过当前顶点到达邻居的距离更短
            if not visited[edge.to] and cur_dist + edge.val < minDist[edge.to]:
                # 更新邻居的最短距离估计值
                minDist[edge.to] = cur_dist + edge.val
                # 将新的距离和邻居顶点入堆。注意:这里入堆的是新的估计值,旧的估计值依然在堆中,但会被上面的continue跳过。
                heapq.heappush(pq, (minDist[edge.to], edge.to))

    # 返回起点到终点的最短距离,若不可达则返回-1
    return -1 if minDist[end] == float('inf') else minDist[end]

核心要点与优化解释

  • 邻接表 (grid)​​:使用邻接表而非邻接矩阵来存储图,特别适合边数较少的稀疏图,节省空间。

  • 最小堆 (pq)​ ​:heapq模块实现了最小堆。堆中的元素是元组 (distance, node)。Python比较元组时按顺序比较,因此以距离作为第一项,能保证堆总是按距离从小到大排序。

  • ​**visited数组的重要性** ​:这是避免重复处理的关键。当一个顶点从堆中弹出时,如果它已经被标记为visited,说明有更短的路径已经确定了它的距离,当前弹出的这个(可能更大的)距离记录就是无效的,直接跳过。

  • 冗余入堆 ​:代码中在更新一个邻居的距离后,会直接将其新距离入堆,而不是先删除堆中该邻居的旧距离。这会导致堆中可能存在同一个顶点的多个不同距离记录。这是一种常见的"惰性删除"优化,虽然增加了堆的大小,但避免了复杂的堆内元素修改操作,在实践中通常更高效。最终的正确性由visited数组和出堆时的检查来保证。

Bellman-Ford 算法是解决带负权边的有向图单源最短路径问题的一种经典算法。下面我将结合你提供的代码,详细解释其原理和实现。

⚙️ 算法核心思想

Bellman-Ford 算法通过动态规划松弛操作 来求解最短路径。其核心步骤是进行最多 V-1轮(V 为顶点数)的松弛操作,确保找到从源点到所有其他顶点的最短路径。如果图中存在从源点可达的负权环,算法能够检测出来。

关键特性​:

  • 处理负权边​:与 Dijkstra 算法不同,Bellman-Ford 允许图中包含负权重的边。

  • 检测负权环​:通过额外的松弛操作可以判断图中是否存在负权环。

  • 简单路径限制 ​:最短路径最多包含 V-1条边,因此最多需要 V-1轮松弛。

算法流程与代码解析

以下流程图展示了 Bellman-Ford 算法的主要步骤和你代码中的关键逻辑:

对应你提供的代码,其执行过程如下:

1. 初始化距离数组
python 复制代码
minDist = [float("inf")] * (n + 1)  # 所有顶点初始距离为无穷大
minDist[1] = 0  # 源点(起点)距离设为0
  • 创建一个数组 minDist,用于记录从源点(顶点1)到各个顶点的当前最短距离估计值。

  • 初始时,源点自身的距离设为0,其他顶点设为无穷大,表示尚未找到路径。

2. 核心松弛操作
python 复制代码
for i in range(1, n):  # 最多进行n-1轮松弛
    updated = False
    for src, dest, weight in edges:  # 遍历所有边
        if minDist[src] != float("inf") and minDist[src] + weight < minDist[dest]:
            minDist[dest] = minDist[src] + weight  # 松弛操作
            updated = True
    if not updated:  # 提前终止优化
        break
  • 外层循环 ​:最多进行 n-1轮。因为任何不含负权环的最短路径最多包含 n-1条边。

  • 内层循环 ​:遍历图中的所有边。对于每条边 (src, dest, weight),进行松弛操作 ​:检查如果从源点经过顶点 src再到顶点 dest的路径距离比当前已知到 dest的最短距离更短,则更新 minDist[dest]

  • 提前终止优化​:如果在一轮松弛中没有任何距离被更新,说明所有最短路径已被找到,可以提前结束循环,减少不必要的计算。

3. 结果返回
复制代码
if minDist[-1] == float("inf"):  # 检查终点是否可达
    return "unconnected"
return minDist[-1]  # 返回到达终点的最短距离
  • 检查目标顶点(通常是编号为 n的顶点)的最短距离是否仍是无穷大。如果是,则表示从源点无法到达该顶点,返回 "unconnected"。

  • 否则,返回计算得到的最短距离。

关键点说明

1. 负权环处理

代码没有显式检测负权环 。标准的 Bellman-Ford 算法在完成 n-1轮松弛后,会再进行一轮松弛 。如果在这一轮中任何顶点的距离还能被更新,则说明图中存在从源点可达的负权环​(因为可以不断绕环降低总路径成本)。添加检测的代码如下:

复制代码
# 在n-1轮松弛之后,添加负权环检测
for src, dest, weight in edges:
    if minDist[src] != float("inf") and minDist[src] + weight < minDist[dest]:
        return "图中存在从源点可达的负权环,无最短路径"

在你的代码中,由于题目可能确保无负权环,或者只关心连通性,因此省略了此步骤。

2. 时间复杂度与空间复杂度
  • 时间复杂度 ​:代码中有两层循环,外层最多 n-1次,内层遍历所有 m条边,因此最坏时间复杂度为 ​**O(nm)​*​。

  • 空间复杂度 ​:主要是存储边集和距离数组,空间复杂度为 ​O(n + m)​

相关推荐
u0109272713 小时前
C++中的RAII技术深入
开发语言·c++·算法
2401_832131954 小时前
模板错误消息优化
开发语言·c++·算法
金枪不摆鳍4 小时前
算法--二叉搜索树
数据结构·c++·算法
近津薪荼4 小时前
优选算法——双指针6(单调性)
c++·学习·算法
helloworldandy4 小时前
高性能图像处理库
开发语言·c++·算法
2401_836563184 小时前
C++中的枚举类高级用法
开发语言·c++·算法
bantinghy5 小时前
Nginx基础加权轮询负载均衡算法
服务器·算法·nginx·负载均衡
chao1898445 小时前
矢量拟合算法在网络参数有理式拟合中的应用
开发语言·算法
代码无bug抓狂人5 小时前
动态规划(附带入门例题)
c语言·算法·动态规划