【图论】最短路径问题总结

一图胜千言

单源最短路径

正权值 朴素Dijkstra

dijkstra算法思想是维护一个永久集合U,全部点集合V

循环n -1

从源点开始,在未被访问的节点中,选择距离源点最近的节点 t

以节点 t 为中间节点,更新从起点到其他节点的最短距离。对于每个节点 j,比较当前的 distance[j]distance[t] + graph[t][j],取较小值作为新的 distance[j]

将节点 t 标记为已访问。

【例题】

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为正值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 −1。

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围

1≤n≤500,

1≤m≤ 1 0 5 10^5 105,

图中涉及边长均不超过10000。

输入样例:

复制代码
3 3
1 2 2
2 3 1
1 3 4

输出样例:

复制代码
3
java 复制代码
import java.io.*;
import java.util.*;
public class Main {
    static final int MAX_NUM = 2147483647 / 2;
    static final int N = 510;
    static final int M = 100010;
    static int[][] graph = new int[N][N];
    static int[] used = new int[N];
    static int[] distance = new int[N];
    static int n, m;
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String row1 = br.readLine();
        String[] items = row1.split(" ");
        n = Integer.parseInt(items[0]);
        m = Integer.parseInt(items[1]);
        //初始化将graph全初始化为无穷大
        for(int i = 0; i <= n; i++) {
            for(int j = 0; j <= n; j++) {
                graph[i][j] = MAX_NUM;
            }
        }
        //存储邻接矩阵
        while(m-- > 0) {
            String row = br.readLine();
            String[] data = row.split(" ");
            int x = Integer.parseInt(data[0]);
            int y = Integer.parseInt(data[1]);
            int z = Integer.parseInt(data[2]);
            //因为可能会有重边,只保存最小的
            graph[x][y] = Math.min(graph[x][y], z);
        }
        System.out.print(dijkstra());
        br.close();
    }
    public static int dijkstra() {
        //将距离初始化为最大值
        for(int i = 0; i <= n; i++) {
            distance[i] = MAX_NUM;
        }
        distance[1] = 0;
        for(int i = 0; i < n - 1; i++) {
            int t = -1;
            //寻找与永久集合中权值最小的结点
            for(int j = 1; j <= n; j++) {
                if(used[j] == 0 && (t == -1 || distance[t] > distance[j])) {
                    t = j;
                }
            }

            //根据t计算从初结点到后序结点和从t结点到后序结点那个值更小
            for(int j = 1; j <= n; j++) {
                distance[j] = Math.min(distance[j], distance[t] + graph[t][j]);
            }

            //标记t为用过结点
            used[t] = 1;
        }
        if(distance[n] == MAX_NUM) {
            return -1;
        } else {
            return distance[n];
        }
    }
}

正权值 堆优化Dijkstra

堆优化版通过优先队列(堆)来快速找到距离源点最近的节点,每次从堆中取出最小元素的时间复杂度为 (O(\log n)),而遍历所有边的时间复杂度为 (O(m))。

朴素版 Dijkstra:适用于稠密图,即边的数量接近节点数量的平方的情况。因为在稠密图中,邻接矩阵可以更方便地存储和访问图的信息。

堆优化版 Dijkstra:适用于稀疏图,即边的数量远小于节点数量的平方的情况。在稀疏图中,邻接表可以节省存储空间,而优先队列可以提高寻找最小距离节点的效率。

核心步骤

当堆不为空时,从堆中取出距离源点最近的节点 no 及其距离 d

如果节点 no 已经确定最短路径,则跳过该节点。

标记节点 no 已经确定最短路径。

遍历节点 no 的所有邻接节点 j,如果通过节点 no 到达节点 j 的距离比当前记录的距离更短,则更新 distance[j] 的值,并将节点 j 及其新的距离加入堆中。

【例题】

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,所有边权均为非负值。

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出−1。

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 −1。

数据范围

1≤n,m≤1.5× 1 0 5 10^5 105,

图中涉及边长均不小于 0,且不超过 10000。

数据保证:如果最短路存在,则最短路的长度不超过 1 0 9 10^9 109。

输入样例:

复制代码
3 3
1 2 2
2 3 1
1 3 4

输出样例:

复制代码
3
java 复制代码
import java.io.*;
import java.util.*;
public class Main {
    static final int N = 100010;
    static final int MAX_NUM = 2147483647 / 2;
    static int[] head = new int[N];
    static int[] value = new int[N];
    static int[] weight = new int[N];
    static int[] ne = new int[N];
    static int[] used = new int[N];
    static int[] distance = new int[N];
    static int n, m, idx;
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] items = br.readLine().split(" ");
        n = Integer.parseInt(items[0]);
        m = Integer.parseInt(items[1]);
        //初始化head数组
        Arrays.fill(head, -1);
        //读取m次数据
        while(m-- > 0) {
            String[] data = br.readLine().split(" ");
            int x = Integer.parseInt(data[0]);
            int y = Integer.parseInt(data[1]);
            int z = Integer.parseInt(data[2]);
            add(x, y, z);
        }
        System.out.print(dijkstra());
    }
    static void add(int from, int to, int w) {
        value[idx] = to;
        weight[idx] = w;
        ne[idx] = head[from];
        head[from] = idx++;
    }
    static int dijkstra() {
        //初始化距离
        Arrays.fill(distance, MAX_NUM);
        distance[1] = 0;
        PriorityQueue<int[]> heap = new PriorityQueue<>((a, b) -> a[0] - b[0]);
        heap.offer(new int[]{0, 1});
        while(!heap.isEmpty()) {
            //从堆中取出离源点距离最小的元素
            int[] item = heap.poll();
            int d = item[0];
            int no = item[1];
            //判断该点是否已经在永久集合中
            if(used[no] == 1) {
                continue;
            }
            used[no] = 1;
            //根据这一点去计算其他通过该点达到的结点的距离是否更新
            for(int i = head[no]; i != -1; i = ne[i]) {
                //结点编号
                int j = value[i];
                if(distance[j] > distance[no] + weight[i]) {
                    distance[j] = distance[no] + weight[i];
                    heap.offer(new int[]{distance[j], j});
                }
            }
        }
        return distance[n] == MAX_NUM ? -1 : distance[n];
    }
}

负权值 bellman-ford

核心逻辑

初始化

  • distance 数组的所有元素初始化为无穷大 MAX_NUM,表示初始时所有节点到源点的距离都是未知的。
  • 将源点(节点 1)的距离 distance[1] 初始化为 0,因为源点到自身的距离为 0。

迭代更新距离

  • 进行 k 次迭代,每次迭代的目的是保证找到的最短路径最多经过 k 条边。
  • 在每次迭代之前,先将 distance 数组复制到 temp 数组中。这一步是为了避免在更新距离时出现数据串联的问题,确保每次更新都是基于上一次迭代的结果。
  • 遍历 edges 列表中的每一条边 e,对于每条边,尝试通过这条边来更新终点 e.to 的最短距离。具体来说,比较当前 distance[e.to]temp[e.from] + e.weight 的大小,取较小值作为新的 distance[e.to]。(对每一条边进行松弛操作)

如果经历至多n - 1次迭代,能够收敛于稳定,否则一定会有负环

负环每走一次都会使得距离变短,导致无穷循环

对于由n个结点构成的链路,最多有n-1跳,所以超过n - 1跳就一定会有负环存在,且不可能是最短路径

【例题】

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出从 1 号点到 n 号点的最多经过 k 条边的最短距离,如果无法从 1 号点走到 n 号点,输出 impossible

注意:图中可能 存在负权回路

输入格式

第一行包含三个整数 n,m,k。

接下来 mm 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

点的编号为 1∼n。

输出格式

输出一个整数,表示从 1 号点到 n 号点的最多经过 k 条边的最短距离。

如果不存在满足条件的路径,则输出 impossible

数据范围

1≤n,k≤500,

1≤m≤10000,

1≤x,y≤n,

任意边长的绝对值不超过 10000。

输入样例:

复制代码
3 3 1
1 2 1
2 3 1
1 3 3

输出样例:

复制代码
3
java 复制代码
import java.io.*;
import java.util.*;
public class Main {
    static class edge {
        public int from;
        public int to;
        public int weight;

        public edge(int from, int to, int weight) {
            this.from = from;
            this.to = to;
            this.weight = weight;
        }
    }
    static final int N = 510;
    static final int MAX_NUM = 2147483647 / 2;
    static int n, m, k;
    static List<edge> edges = new ArrayList<>();
    static int[] distance = new int[N];
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] items = br.readLine().split(" ");
        n = Integer.parseInt(items[0]);
        m = Integer.parseInt(items[1]);
        k = Integer.parseInt(items[2]);
        while (m-- > 0) {
            String[] data = br.readLine().split(" ");
            int from = Integer.parseInt(data[0]);
            int to = Integer.parseInt(data[1]);
            int weight = Integer.parseInt(data[2]);
            edges.add(new edge(from, to, weight));
        }
        //调用bellman ford算法
       bellmanFord();
    }
    public static void bellmanFord() {
        //初始化distance
        Arrays.fill(distance, MAX_NUM);
        distance[1] = 0;
        //k次循环保证最多经过k条边的距离
        for (int i = 0; i < k; i++) {
            //拷贝distance数组,避免数据串联
            int[] temp = Arrays.copyOf(distance, distance.length);
            for (edge e : edges) {
                distance[e.to] = Math.min(distance[e.to], temp[e.from] + e.weight );
            }
        }
        //判断distance[n]的大小, MAX_NUM / 2 是终点前的负值边对对distance[n]产生影响,会使最大值减少 k * weight 
        if (distance[n] > MAX_NUM / 2) {
            System.out.println("impossible");
        } else {
            System.out.println(distance[n]);
        }
    }
}

负权值 SPFA

Bellman - Ford 算法的时间复杂度是 (O(k m)),其中 k 通常是节点数 n,也就是在一般情况下时间复杂度为 (O(n m))。这是因为在每一轮迭代中,它都会对图中的每一条边进行松弛操作,不管这条边是否能真正更新节点的最短距离。在很多情况下,大量的松弛操作是不必要的,导致算法效率较低。

当图的规模较大时,Bellman - Ford 算法的性能会变得很差。而 SPFA(Shortest Path Faster Algorithm)算法就是为了优化 Bellman - Ford 算法的效率而提出的,它通过队列来减少不必要的松弛操作,从而在很多情况下能显著提高算法的执行效率。

SPFA 算法的核心思想是利用队列来维护待处理的节点。只有当一个节点的最短距离被更新时,才将其加入队列,等待后续对其出边进行松弛操作。这样就避免了 Bellman - Ford 算法中对所有边进行无意义的松弛操作,从而减少了不必要的计算。

初始化

  • 定义常量 N 表示节点的最大数量,MAX_NUM 表示无穷大。
  • 初始化邻接表相关数组 headvalueweightne 用于存储图的信息。
  • 初始化队列 queue,队头指针 hh 和队尾指针 tt
  • 初始化 distance 数组,将所有节点的距离初始化为无穷大,源点(节点 1)的距离初始化为 0。
  • 初始化 inQueue 数组,用于标记节点是否在队列中,初始时所有节点都不在队列中。

入队操作

将源点(节点 1)加入队列,并标记其在队列中。

队列处理

  • 当队列不为空时,从队头取出一个节点 t,并标记该节点不在队列中。

  • 遍历节点t的所有出边:

    • 对于每一条出边 (t, j),如果通过节点 t 到达节点 j 的距离比当前记录的 distance[j] 更短,则更新 distance[j] 的值。
    • 如果节点 j 不在队列中,则将其加入队列,并标记其在队列中。

【例题】

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环, 边权可能为负数

请你求出 1 号点到 n 号点的最短距离,如果无法从 1 号点走到 n 号点,则输出 impossible

数据保证不存在负权回路。

输入格式

第一行包含整数 n 和 m。

接下来 m 行每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

输出格式

输出一个整数,表示 1 号点到 n 号点的最短距离。

如果路径不存在,则输出 impossible

数据范围

1≤n,m≤ 1 0 5 10^5 105,

图中涉及边长绝对值均不超过 10000。

输入样例:

复制代码
3 3
1 2 5
2 3 -3
1 3 4

输出样例:

复制代码
2
java 复制代码
import java.io.*;
import java.util.*;
public class Main {
    static final int N = 100010;
    static final int MAX_NUM = 2147483647 / 2;
    static int[] head = new int[N];
    static int[] value = new int[N];
    static int[] weight = new int[N];
    static int[] ne = new int[N];
    static int hh = 0, tt = -1, idx = 0, n, m;
    static int[] queue = new int[N];
    static int[] distance = new int[N];
    static boolean[] inQueue = new boolean[N];
    public static void main(String[] args) throws Exception{
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] row1 = br.readLine().split(" ");
        n = Integer.parseInt(row1[0]);
        m = Integer.parseInt(row1[1]);
        //初始化head数组
        Arrays.fill(head, -1);
        //m次读入
        while(m-- > 0) {
            String[] data = br.readLine().split(" ");
            int x = Integer.parseInt(data[0]);
            int y = Integer.parseInt(data[1]);
            int z = Integer.parseInt(data[2]);
            add(x, y, z);
        }
        spfa();
        br.close();
    }
    static void add(int from, int to, int w) {
        value[idx] = to;
        weight[idx] = w;
        ne[idx] = head[from];
        head[from] = idx++;
    }
    static void spfa() {
        //初始化distance数组
        Arrays.fill(distance, MAX_NUM);
        distance[1] = 0;
        //将第一个数加入队列
        queue[++tt] = 1;
        //更新1的状态
        inQueue[1] = true;
        while(hh <= tt) {
            //从队头取出一个数
            int t = queue[hh++];
            //更新队头元素的状态
            inQueue[t] = false;
            //遍历该结点的所有出边
            for(int i = head[t]; i != -1; i = ne[i]) {
                int j = value[i];
                if(distance[j] > distance[t] + weight[i]) {
                    distance[j] = distance[t] + weight[i];
                    if(inQueue[j] == false) {
                        queue[++tt] = j;
                        inQueue[j] = true;
                    }
                }
            }
        }
        if(distance[n] == MAX_NUM) {
            System.out.print("impossible");
        } else {
            System.out.print(distance[n]);
        }
    }
}

多源最短路径

Floyd

算法原理

Floyd 算法通过一个三层循环来逐步更新图中各顶点对之间的最短路径。设图中有 n 个顶点,用邻接矩阵 (graph[i][j]) 表示顶点 i 到顶点 j 的边权(若 i 和 j 之间没有边,则权值为无穷大)。

定义一个三维数组 (dist[k][i][j]) 表示从顶点 i 到顶点 j 经过编号不超过 k 的顶点的最短路径长度(也可简化为二维数组 (dist[i][j]) ,在每次迭代中直接更新)。

迭代过程分析

  • 初始状态:在算法开始时,(dist[0][i][j]=graph[i][j]) ,即不经过任何中间顶点时,顶点 i 到顶点 j 的距离就是它们之间的边权。这是符合实际情况的,因为没有中间节点参与时,两点间距离就是直接相连的边权(若不相连则为无穷大)。
  • 第 k 次迭代:在第 k 次迭代中,对于每一对顶点 ((i, j)) ,考虑是否经过顶点 k 会使 i 到 j 的路径更短。即比较 (dist[k - 1][i][j]) (不经过顶点 k 时 i 到 j 的最短路径)和 (dist[k - 1][i][k]+dist[k - 1][k][j]) (经过顶点 k ,从 i 到 k 再从 k 到 j 的路径长度 )的大小。取较小值作为 (dist[k][i][j]) 。

这种比较是合理的,因为如果存在一条从 i 到 j 经过顶点 k 的更短路径,那么必然是由从 i 到 k 的最短路径和从 k 到 j 的最短路径组成。而在第 k 次迭代时,我们已经知道了不经过顶点 k (即经过编号小于 k 的顶点子集 )时从 i 到 k 和从 k 到 j 的最短路径(分别为 (dist[k - 1][i][k]) 和 (dist[k - 1][k][j]) ) 。通过这种比较和更新,我们能得到经过编号不超过 k 的顶点时 i 到 j 的最短路径。

【例题】

给定一个 n 个点 m 条边的有向图,图中可能存在重边和自环,边权可能为负数。

再给定 k 个询问,每个询问包含两个整数 x 和 y,表示查询从点 x 到点 y 的最短距离,如果路径不存在,则输出 impossible

数据保证图中不存在负权回路。

输入格式

第一行包含三个整数 n,m,k。

接下来 m 行,每行包含三个整数 x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。

接下来 k 行,每行包含两个整数 x,y,表示询问点 xx 到点 y 的最短距离。

输出格式

共kk 行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible

数据范围

1≤n≤200,

1≤k≤n2

1≤m≤20000,

图中涉及边长绝对值均不超过 10000。

输入样例:

复制代码
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3

输出样例:

复制代码
impossible
1
java 复制代码
import java.io.*;
import java.util.*;
public class Main {
    static final int MAX_NUM = 2147483647 / 2;
    static final int N = 210;
    static int n, m;
    static int[][] graph = new int[N][N];
    public static void main(String[] args) throws Exception {
        BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
        String[] row1 = br.readLine().split(" ");
        n = Integer.parseInt(row1[0]);
        m = Integer.parseInt(row1[1]);
        int q = Integer.parseInt(row1[2]);
        //对邻接矩阵进行初始化
        for(int i = 1; i <= n; i++) {
            for(int j = 1; j <= n; j++) {
                if(i == j) {
                    graph[i][j] = 0;
                } else {
                    graph[i][j] = MAX_NUM;
                }
            }
        }
        for(int i = 1; i <= m; i++) {
          String[] data = br.readLine().split(" ");
          int a = Integer.parseInt(data[0]);
          int b = Integer.parseInt(data[1]);
          int c = Integer.parseInt(data[2]);
          graph[a][b] = Math.min(graph[a][b], c);
        }
        floyd();
        //q次询问
        while(q-- > 0) {
            String[] fromTo = br.readLine().split(" ");
            int from = Integer.parseInt(fromTo[0]);
            int to = Integer.parseInt(fromTo[1]);
            if(graph[from][to] > MAX_NUM / 2) {
                System.out.println("impossible");
            } else {
                System.out.println(graph[from][to]);
            }
        }
    }
    static void floyd() {
        for(int k = 1; k <= n; k++) {
            for(int i = 1; i <= n; i++) {
                for(int j = 1; j <= n; j++) {
                    graph[i][j] = Math.min(graph[i][j], graph[i][k] + graph[k][j]);
                }
            }
        }
    }
}
相关推荐
佚名涙1 分钟前
go中锁的入门到进阶使用
开发语言·后端·golang
猫猫的小茶馆3 分钟前
【PCB工艺】软件是如何控制硬件的发展过程
开发语言·stm32·单片机·嵌入式硬件·mcu·51单片机·pcb工艺
勘察加熊人1 小时前
wpf+c#路径迷宫鼠标绘制
开发语言·c#·wpf
在京奋斗者1 小时前
spring boot自动装配原理
java·spring boot·spring
小黄人软件2 小时前
C# ini文件全自动界面配置:打开界面时读ini配置到界面各控件,界面上的控件根据ini文件内容自动生成,点保存时把界面各控件的值写到ini里。
开发语言·c#
明天不下雨(牛客同名)4 小时前
为什么 ThreadLocalMap 的 key 是弱引用 value是强引用
java·jvm·算法
多多*4 小时前
Java设计模式 简单工厂模式 工厂方法模式 抽象工厂模式 模版工厂模式 模式对比
java·linux·运维·服务器·stm32·单片机·嵌入式硬件
Android洋芋5 小时前
C语言深度解析:从零到系统级开发的完整指南
c语言·开发语言·stm32·条件语句·循环语句·结构体与联合体·指针基础
bjxiaxueliang5 小时前
一文详解QT环境搭建:Windows使用CLion配置QT开发环境
开发语言·windows·qt
Run_Teenage5 小时前
C语言 【初始指针】【指针一】
c语言·开发语言