代码随想录第五十九天| dijkstra(堆优化版)精讲 Bellman_ford 算法精讲

Dijkstra算法(堆优化版)精讲

题目描述

小明需要从第一个车站出发,到达最后一个车站参加科学大会。途中各车站之间的道路状况、交通拥堵程度等因素会影响通行时间。小明希望选择一条时间最短的路线。

输入包含车站数量N和公路数量M,接下来M行每行三个整数S、E、V,表示从S车站到E车站的单向道路,花费V单位时间。输出从起点到终点的最短时间,若无法到达则输出-1。

输入输出示例

输入:

7 9
1 2 1
1 3 4
2 3 2
2 4 5
3 4 2
4 5 3
2 6 4
5 7 4
6 7 9

输出:

12

解题思路

算法原理

Dijkstra算法用于求解单源最短路径问题,适用于有权重的有向或无向图,且所有边的权重非负。堆优化版Dijkstra算法通过优先队列(小顶堆)来优化节点选择过程,降低时间复杂度。

图的存储

使用邻接表存储图结构,邻接表由数组和链表组成,每个节点对应一个链表,链表中存储该节点直接指向的其他节点及边的权重。

堆优化细节

  1. 优先队列:用优先队列(小顶堆)存储待处理的节点,按源点到该节点的当前最短距离排序,每次取出距离最小的节点进行处理。
  2. 节点选择:通过堆顶元素直接获取距离源点最近的未访问节点,避免遍历所有节点查找最小距离。
  3. 距离更新:处理当前节点时,遍历其邻接节点,若通过当前节点到达邻接节点的距离更短,则更新距离并将其加入优先队列。

代码实现

import java.util.*;

class Edge {
    int to;  // 邻接顶点
    int val; // 边的权重

    Edge(int to, int val) {
        this.to = to;
        this.val = val;
    }
}

class MyComparison implements Comparator<Pair<Integer, Integer>> {
    @Override
    public int compare(Pair<Integer, Integer> lhs, Pair<Integer, Integer> rhs) {
        return Integer.compare(lhs.second, rhs.second);
    }
}

class Pair<U, V> {
    public final U first;
    public final V second;

    public Pair(U first, V second) {
        this.first = first;
        this.second = second;
    }
}

public class Main {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();
        int m = scanner.nextInt();

        List<List<Edge>> grid = new ArrayList<>(n + 1);
        for (int i = 0; i <= n; i++) {
            grid.add(new ArrayList<>());
        }

        for (int i = 0; i < m; i++) {
            int p1 = scanner.nextInt();
            int p2 = scanner.nextInt();
            int val = scanner.nextInt();
            grid.get(p1).add(new Edge(p2, val));
        }

        int start = 1;  // 起点
        int end = n;    // 终点

        // 存储从源点到每个节点的最短距离
        int[] minDist = new int[n + 1];
        Arrays.fill(minDist, Integer.MAX_VALUE);

        // 记录顶点是否被访问过
        boolean[] visited = new boolean[n + 1];

        // 优先队列中存放 Pair<节点,源点到该节点的权值>
        PriorityQueue<Pair<Integer, Integer>> pq = new PriorityQueue<>(new MyComparison());

        // 初始化队列,源点到源点的距离为0,所以初始为0
        pq.add(new Pair<>(start, 0));

        minDist[start] = 0;  // 起始点到自身的距离为0

        while (!pq.isEmpty()) {
            // 1. 第一步,选源点到哪个节点近且该节点未被访问过(通过优先级队列来实现)
            // <节点, 源点到该节点的距离>
            Pair<Integer, Integer> cur = pq.poll();

            if (visited[cur.first]) continue;

            // 2. 第二步,该最近节点被标记访问过
            visited[cur.first] = true;

            // 3. 第三步,更新非访问节点到源点的距离(即更新minDist数组)
            for (Edge edge : grid.get(cur.first)) { // 遍历 cur指向的节点,cur指向的节点为 edge
                // cur指向的节点edge.to,这条边的权值为 edge.val
                if (!visited[edge.to] && minDist[cur.first] + edge.val < minDist[edge.to]) { // 更新minDist
                    minDist[edge.to] = minDist[cur.first] + edge.val;
                    pq.add(new Pair<>(edge.to, minDist[edge.to]));
                }
            }
        }

        if (minDist[end] == Integer.MAX_VALUE) {
            System.out.println(-1); // 不能到达终点
        } else {
            System.out.println(minDist[end]); // 到达终点最短路径
        }
    }
}

代码注释

  1. Edge类:表示图中的边,包含目标节点编号和权重。
  2. MyComparison类:用于优先队列的比较器,按距离大小排序。
  3. Pair类:存储节点编号和对应的距离。
  4. 主函数
    • 读取输入,构建邻接表。
    • 初始化距离数组和访问标记数组。
    • 使用优先队列辅助Dijkstra算法,每次取出距离最小的节点,更新其邻接节点的距离。
    • 最终输出终点的最短距离或-1。

总结

堆优化版Dijkstra算法通过优先队列优化节点选择过程,将时间复杂度从O(n^2)降低到O((n + m) log n),适合处理稀疏图。理解邻接表的存储方式和优先队列的使用是掌握该算法的关键。

Bellman-Ford 算法精讲

题目描述

某国有 n 个城市,通过 m 条单向道路连接。每条道路有运输成本和政府补贴,权值为运输成本减去政府补贴。权值为正表示需支付费用,为负表示能盈利。要求找出从城市 1 到城市 n 的最低运输成本路径,若成本为负则表示盈利,若无路径则输出 “unconnected”。

输入包含 n 和 m,接着 m 行每行三个整数 s、t、v,表示从 s 到 t 的道路权值为 v。输出为最低成本或 “unconnected”。

输入输出示例

输入:

6 7
5 6 -2
1 2 1
5 3 1
2 5 2
2 4 -3
4 6 4
1 3 5

输出:

-4

解题思路

算法原理

Bellman-Ford 算法用于解决单源最短路径问题,尤其适用于图中存在负权边的情况。其核心思想是对所有边进行 n-1 次松弛操作,逐步更新源点到各节点的最短距离。

关键概念

  • 松弛操作:对于一条边 (u, v) 权值为 w,若当前记录的源点到 v 的最短距离大于源点到 u 的最短距离加上 w,则更新源点到 v 的最短距离。这是 Bellman-Ford 算法的核心操作。
  • 松弛次数:图中节点数为 n,最多经过 n-1 条边即可从源点到达任意其他节点,因此需要对所有边进行 n-1 次松弛操作。

代码实现

public class Main {

    // 定义边的类,包含起点、终点和权值
    static class Edge {
        int from;
        int to;
        int val;
        public Edge(int from, int to, int val) {
            this.from = from;
            this.to = to;
            this.val = val;
        }
    }

    public static void main(String[] args) {
        // 输入处理
        Scanner sc = new Scanner(System.in);
        int n = sc.nextInt(); // 城市数量
        int m = sc.nextInt(); // 道路数量
        List<Edge> edges = new ArrayList<>(); // 存储所有边

        // 读取每条道路的信息
        for (int i = 0; i < m; i++) {
            int from = sc.nextInt();
            int to = sc.nextInt();
            int val = sc.nextInt();
            edges.add(new Edge(from, to, val));
        }

        // 初始化最短距离数组
        int[] minDist = new int[n + 1];
        Arrays.fill(minDist, Integer.MAX_VALUE); // 初始时所有距离设为最大值
        minDist[1] = 0; // 源点到自身的距离为0

        // 进行n-1次松弛操作
        for (int i = 1; i < n; i++) {
            // 遍历所有边,尝试更新最短距离
            for (Edge edge : edges) {
                // 如果当前边的起点的最短距离不是最大值,并且可以通过这条边更新终点的最短距离
                if (minDist[edge.from] != Integer.MAX_VALUE && (minDist[edge.from] + edge.val) < minDist[edge.to]) {
                    minDist[edge.to] = minDist[edge.from] + edge.val; // 更新最短距离
                }
            }
        }

        // 输出结果
        if (minDist[n] == Integer.MAX_VALUE) {
            System.out.println("unconnected"); // 无法到达终点
        } else {
            System.out.println(minDist[n]); // 输出最短距离
        }
    }
}

代码注释

  1. Edge类:用于表示图中的每一条边,包含起点、终点和权值。
  2. 输入处理:读取城市数量和道路数量,然后读取每条道路的信息并存储在列表中。
  3. 初始化:创建一个数组 minDist 用于存储源点到各节点的最短距离,初始时所有距离设为最大值,源点到自身的距离设为0。
  4. 松弛操作:对所有边进行 n-1 次松弛,每次遍历所有边,尝试通过当前边更新终点的最短距离。
  5. 结果输出:检查终点的最短距离是否仍为最大值,若是则说明无法到达,否则输出该距离。

总结

Bellman-Ford 算法通过多次松弛操作逐步求解单源最短路径问题,能够处理图中存在负权边的情况。其时间复杂度为 O(n*m),其中 n 是节点数,m 是边数。理解松弛操作的含义和多次松弛的必要性是掌握该算法的关键.

Dijkstra 算法与 Bellman-Ford 算法对比

适用场景

  • Dijkstra 算法:适用于所有边权值非负的图,用于求解单源最短路径问题。
  • Bellman-Ford 算法:适用于可能存在负权边的图,同样用于求解单源最短路径问题,但图中不能存在负权回路。

时间复杂度

  • Dijkstra 算法:在使用优先队列优化的情况下,时间复杂度为 O((n + m) log n),其中 n 是节点数,m 是边数。在稠密图中表现较好。
  • Bellman-Ford 算法:时间复杂度为 O(n * m),对于每个节点都要进行 n-1 次松弛操作。在边数较多时效率较低。

空间复杂度

  • Dijkstra 算法:需要存储图的结构(邻接表或邻接矩阵)以及优先队列,空间复杂度通常为 O(n + m)。
  • Bellman-Ford 算法:需要存储图的边列表以及距离数组,空间复杂度为 O(m + n)。

算法思想

  • Dijkstra 算法:基于贪心策略,每次选择距离源点最近的未访问节点,更新其邻接节点的距离。利用优先队列优化节点选择过程。
  • Bellman-Ford 算法:基于动态规划思想,通过多次松弛操作逐步更新源点到各节点的最短距离,直到无法再更新为止。

优缺点

Dijkstra 算法

  • 优点
    • 时间效率高,在无负权边的图中表现优异。
    • 可以处理大规模图问题。
  • 缺点
    • 无法处理存在负权边的图。

Bellman-Ford 算法

  • 优点
    • 能够处理存在负权边的图(无负权回路)。
    • 可以检测图中是否存在负权回路。
  • 缺点
    • 时间效率较低,尤其在边数较多时。
    • 空间占用相对较高。

选择依据

  • 当图中不存在负权边时,优先选择 Dijkstra 算法,尤其是图较为稀疏或节点数量较大时。
  • 当图中存在负权边且无负权回路时,选择 Bellman-Ford 算法。
  • 若需要检测图中是否存在负权回路,Bellman-Ford 算法是合适的选择。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值