代码随想录算法训练营第59天 | 图论part09:dijkstra(堆优化版)精讲、Bellman_ford 算法精讲

第十一章:图论part09

今天的建议依然是,一刷的时候,能了解 原理,照着代码随想录能抄下来代码就好,就算达标。
二刷的时候自己尝试独立去写,三刷的时候 才能有一定深度理解各个最短路算法。


dijkstra(堆优化版)精讲

文章讲解

思路

在之前的Dijkstra算法中,我们通过遍历所有节点来寻找未访问的最近节点。这种方法在稠密图(即边的数量很多)中表现良好,但在稀疏图(边的数量较少)中,效率较低。因此,我们可以通过使用堆(优先队列)和邻接表来优化Dijkstra算法,使其更适用于稀疏图。

一、图的存储方式

图的存储方式主要有两种:邻接矩阵和邻接表。这两种存储方式各有优缺点,适用于不同的场景。

  1. 邻接矩阵(Adjacency Matrix):
    结构: 使用二维数组来表示图。矩阵中的元素grid[i][j]表示节点i到节点j的边的权重。如果grid[i][j]是一个很大的值(如Integer.MAX_VALUE),表示节点i和节点j之间没有直接连接的边。
    优点: 检查任意两个节点之间是否存在边非常快速,适合稠密图(边数接近顶点数平方的图)。
    缺点: 对于稀疏图(边数远小于顶点数平方的图),会导致大量空间浪费。此外,在遍历邻接节点时效率较低,因为需要遍历整个矩阵。

  2. 邻接表(Adjacency List):
    结构: 使用数组加链表(或数组加动态数组)的方式来表示图。每个节点对应一个链表,链表中的元素表示与该节点相邻的其他节点及其边的权重。
    优点: 对于稀疏图,邻接表只存储实际存在的边,节省空间。在遍历邻接节点时,效率较高。
    缺点: 检查任意两个节点之间是否存在边的效率相对较低。

二、算法实现
import java.util.*;

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

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

class DijkstraWithHeap {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(); // 节点数量
        int m = scanner.nextInt(); // 边的数量

        // 构建邻接表
        List<List<Edge>> graph = new ArrayList<>();
        for (int i = 0; i <= n; i++) {
            graph.add(new ArrayList<>()); // 初始化邻接表
        }

        // 读取边的信息
        for (int i = 0; i < m; i++) {
            int u = scanner.nextInt();
            int v = scanner.nextInt();
            int w = scanner.nextInt();
            graph.get(u).add(new Edge(v, w)); // 记录边的权重
        }

        // Dijkstra算法的堆优化实现
        int start = 1; // 起始节点
        int[] minDist = new int[n + 1]; // 存储从源点到每个节点的最短距离
        boolean[] visited = new boolean[n + 1]; // 记录节点是否已被访问过
        Arrays.fill(minDist, Integer.MAX_VALUE);
        minDist[start] = 0;

        // 使用优先队列(小顶堆)来优化
        PriorityQueue<int[]> pq = new PriorityQueue<>(Comparator.comparingInt(pair -> pair[1]));
        pq.add(new int[]{start, 0});

        while (!pq.isEmpty()) {
            int[] cur = pq.poll();
            int curNode = cur[0];
            int curDist = cur[1];

            if (visited[curNode]) continue; // 如果该节点已被访问,跳过
            visited[curNode] = true; // 标记该节点已被访问

            // 遍历该节点的邻接边
            for (Edge edge : graph.get(curNode)) {
                int nextNode = edge.to;
                int weight = edge.val;

                if (!visited[nextNode] && curDist + weight < minDist[nextNode]) {
                    minDist[nextNode] = curDist + weight; // 更新最短路径
                    pq.add(new int[]{nextNode, minDist[nextNode]}); // 将新节点及其距离加入堆中
                }
            }
        }

        // 输出结果
        for (int i = 1; i <= n; i++) {
            if (minDist[i] == Integer.MAX_VALUE) {
                System.out.println("Node " + i + " is unreachable from the source.");
            } else {
                System.out.println("Shortest distance to node " + i + ": " + minDist[i]);
            }
        }
    }
}

三、算法的步骤

无论是朴素版还是堆优化版,Dijkstra算法的基本步骤是相同的:

  1. 初始化:
    将所有节点的最短距离初始化为无穷大(Integer.MAX_VALUE),将源点的最短距离初始化为0。

  2. 选取未访问的最短距离节点:
    朴素版通过遍历所有节点找到距离源点最近的未访问节点。
    堆优化版通过小顶堆直接获取距离源点最近的未访问节点。

  3. 标记该节点为已访问。

  4. 更新其邻接节点的最短距离:
    对于该节点的每一个邻接节点,如果从源点经过该节点到达邻接节点的距离更短,则更新该邻接节点的最短距离。

  5. 重复步骤2-4,直到所有节点都被访问或所有剩余节点不可达。


Bellman_ford 算法精讲

文章讲解

思路

  • 本题不同之处在于 边的权值是有负数了。
  • Bellman_ford算法的核心思想是 对所有边进行松弛n-1次操作(n为节点数量),从而求得目标最短路。
松弛(Relaxation)是什么?

松弛是图算法中的一个关键概念,尤其在求解最短路径问题时,松弛操作被广泛使用。简单来说,松弛是通过检查并更新当前路径,使得路径更短、更优的过程。

松弛操作的核心思想:

  • 起点:每条边都有一个起点(A)和一个终点(B),以及一个边的权值(value)。
  • 当前最短路径minDist[B]表示当前已知的从源点到终点B的最短路径。
  • 松弛操作:通过检查从源点到达终点B的另一种路径,即先到达A,再通过边A -> B到达B,是否比当前路径minDist[B]更短。如果更短,则更新minDist[B]

公式表达

if (minDist[B] > minDist[A] + value) {
    minDist[B] = minDist[A] + value;
}

这个过程就是所谓的“松弛”。如果通过A到达B的路径更短,我们就“放松”当前路径,使得minDist[B]变得更小。

为什么需要进行n-1次松弛?

在Bellman-Ford算法中,松弛操作是核心步骤。为了确保找到从源点到所有其他节点的最短路径,需要对所有边进行n-1次松弛,原因如下:

  • 图的最大路径长度:在一个包含n个节点的图中,任意两点之间的最短路径最多经过n-1条边。这是因为如果一个路径包含更多的边,必然会经过某些节点多次,意味着存在环。

  • 逐步扩展最短路径:每次松弛都会将当前节点的最短路径信息传播给它的邻接节点。第k次松弛后,所有经过最多k条边的最短路径都会被正确计算出来。因此,经过n-1次松弛操作,最短路径必然会被完全计算出来。

  • 模拟过程参见文章

代码实现
import java.util.*;

public class BellmanFord {
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt(); // 节点数量
        int m = scanner.nextInt(); // 边的数量

        List<int[]> edges = new ArrayList<>();

        // 将所有边保存起来
        for (int i = 0; i < m; i++) {
            int p1 = scanner.nextInt(); // 边的起点
            int p2 = scanner.nextInt(); // 边的终点
            int val = scanner.nextInt(); // 边的权值
            edges.add(new int[]{p1, p2, val});
        }

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

        int[] minDist = new int[n + 1];
        Arrays.fill(minDist, Integer.MAX_VALUE); // 初始化最短距离数组
        minDist[start] = 0;

        // 对所有边松弛 n-1 次
        for (int i = 1; i < n; i++) { 
            for (int[] edge : edges) { // 每一次松弛,都是对所有边进行松弛
                int from = edge[0]; // 边的出发点
                int to = edge[1]; // 边的到达点
                int price = edge[2]; // 边的权值
                
                // 松弛操作 
                // minDist[from] != Integer.MAX_VALUE 防止从未计算过的节点出发
                if (minDist[from] != Integer.MAX_VALUE && minDist[to] > minDist[from] + price) { 
                    minDist[to] = minDist[from] + price;  
                }
            }
        }

        if (minDist[end] == Integer.MAX_VALUE) {
            System.out.println("unconnected"); // 不能到达终点
        } else {
            System.out.println(minDist[end]); // 到达终点的最短路径
        }
    }
}
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,233评论 6 495
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,357评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 159,831评论 0 349
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,313评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,417评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,470评论 1 292
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,482评论 3 412
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,265评论 0 269
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,708评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,997评论 2 328
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,176评论 1 342
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,827评论 4 337
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,503评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,150评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,391评论 1 267
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,034评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,063评论 2 352

推荐阅读更多精彩内容