最短路

最短路

引入

在图中,求出某一点到任意一点的最短路径

单源最短路

例题:P3371 【模板】单源最短路径(弱化版) - 洛谷

Dijkstra

Dijkstra 是一种求解 非负权图 上单源最短路径的算法。

定义 S 为要求的单源最短路的源点

dis(i)Si 的最短路径

松弛

在理解 Dijkstra 算法过程之前,要先理解松弛操作

我们尝试用 Suv(其中 Su 的路径取最短路)这条路径去更新 点 v 最短路的长度,如果这条路径更优,就进行更新。

简单来说就是选择更短的路径

对于边 (u,v),松弛操作对应下面的式子:

dis(v)=min(dis(v),dis(u)+w(u,v))

过程

将结点分成两个集合:

  1. 已确定最短路长度的点集(记为 S 集合)。
  2. 未确定最短路长度的点集(记为 T 集合)。

一开始所有的点都属于 T 集合。

初始化 dis(S)=0 ,其他点的 dis 均为 +

然后重复这些操作:

  1. T 集合中,选取一个最短路长度最小的结点,移到 S 集合中。
  2. 对那些刚刚被加入 S 集合的结点的所有出边执行松弛操作。

直到 T 集合为空,算法结束。

dis(i) 仍为+,则 Si 不可达

时间复杂度

对于操作 1 中选取最短路长度最小的节点有多种方法

采取不同方法有着不同的时间复杂度,以下给出两种常用的方法及总时间复杂度:

  1. 暴力:不使用任何数据结构进行维护,每次 2 操作执行完毕后,直接在 T 集合中暴力寻找最短路长度最小的结点。2 操作总时间复杂度为 O(m)1 操作总时间复杂度为 O(n2),全过程的时间复杂度为 O(n2+m)=O(n2)
  2. 优先队列:每成功松弛一条边 (u,v),就将 v 加入优先队列中,如果同一个点的最短路被更新多次,但对于优先队列前面的元素不能删除也不能修改,只能留在队列中,因此优先队列内的元素个数是 O(m) 的,总时间复杂度为 O(mlog(m))

实现(采用邻接表存图)

1 暴力:

import java.util.Arrays;
import java.util.LinkedList;
import java.util.Scanner;

public class Main {
    static class edge {
        int v;
        long w;

        public edge(int v, long w) {
            this.v = v;
            this.w = w;
        }
    }

    static LinkedList<edge>[] adj;
    //dis[i]表示 S -> i 的最短路长度
    static long[] dis;
    //vis[i]判断i号节点是否被选择
    static boolean[] vis;
    // n个点,m条边
    static int n, m;
    //不要赋值为MAX,程序可能会进行加法等操作导致溢出
    static final long INF = (long) 1e12;

    // 求以start为源点的单源最短路
    static void Dijkstra(int start) {
        Arrays.fill(dis, INF);
        Arrays.fill(vis, false);
        //自己到自己的最短路为0
        dis[start] = 0;
        for (int i = 1; i <= n; ++i) {
            //操作1:在未确定的集合T中选择最短路长度最小的结点
            int u = 0;
            long min = INF;
            for (int j = 1; j <= n; ++j)
                if (!vis[j] && dis[j] < min) {
                    u = j;
                    min = dis[j];
                }
            //如果u为初始值,则说明所有节点都被选择了
            if (u == -1) return;
            //将其加入集合S中
            vis[u] = true;
            //操作2:对加入的结点的所有出边进行松弛操作
            for (edge e : adj[u])
                //松弛操作
                if (dis[e.v] > dis[u] + e.w)
                    dis[e.v] = dis[u] + e.w;
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        //点的标号从1开始,即1~n
        adj = new LinkedList[n + 1];
        for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
        dis = new long[n + 1];
        vis = new boolean[n + 1];
        // start为源点
        int start = sc.nextInt();
        for (int i = 0; i < m; ++i) {
            int u = sc.nextInt(), v = sc.nextInt();
            long w = sc.nextLong();
            // 加边(有向边)
            adj[u].add(new edge(v, w));
        }
        Dijkstra(start);
        for (int i = 1; i <= n; ++i) System.out.printf("%d到%d的最短路径长度为%d\n", start, i, dis[i]);
    }
}

2 优先队列:

import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Scanner;

public class Main {
    static class edge {
        int v;
        long w;

        public edge(int v, long w) {
            this.v = v;
            this.w = w;
        }
    }

    static LinkedList<edge>[] adj;
    //dis[i]表示 S -> i 的最短路长度
    static long[] dis;
    //vis[i]判断i号节点是否被选择
    static boolean[] vis;
    // n个点,m条边
    static int n, m;
    //不要赋值为MAX,程序可能会进行加法等操作导致溢出
    static final long INF = (long) 1e12;

    // 求以start为源点的单源最短路
    static void Dijkstra(int start) {
        Arrays.fill(dis, INF);
        Arrays.fill(vis, false);
        class node {
            int u;
            long dis;

            public node(int u, long dis) {
                this.u = u;
                this.dis = dis;
            }
        }
        PriorityQueue<node> q = new PriorityQueue<>(((o1, o2) -> (int) (o1.dis - o2.dis)));
        //自己到自己的最短路为0
        dis[start] = 0;
        q.add(new node(start, 0));
        while (!q.isEmpty()) {
            //操作1:选择最短路长度最小的结点
            int u = q.poll().u;
            //如果已经在集合中,则选择下一个节点
            if (vis[u]) continue;
            vis[u] = true;
            //操作2:对加入的结点的所有出边进行松弛操作
            for (edge e : adj[u])
                //松弛操作
                if (dis[e.v] >  dis[u] + e.w) {
                    dis[e.v] = dis[u] + e.w;
                    q.add(new node(e.v, dis[e.v]));
                }
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        //点的标号从1开始,即1~n
        adj = new LinkedList[n + 1];
        for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
        dis = new long[n + 1];
        vis = new boolean[n + 1];
        // start为源点
        int start = sc.nextInt();
        for (int i = 0; i < m; ++i) {
            int u = sc.nextInt(), v = sc.nextInt();
            long w = sc.nextLong();
            // 加边(有向边)
            adj[u].add(new edge(v, w));
        }
        Dijkstra(start);
        for (int i = 1; i <= n; ++i) System.out.printf("%d到%d的最短路径长度为%d\n", start, i, dis[i]);
    }
}

测试数据

输入:
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
11的最短路径长度为0
12的最短路径长度为2
13的最短路径长度为4
14的最短路径长度为3

输出路径

每次更新最短路径时,通过数组将 来结点(前驱节点)记录下来

以下给出使用优先队列的输出路径

import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;
import java.util.Scanner;

public class Main {
    static class edge {
        int v;
        long w;

        public edge(int v, long w) {
            this.v = v;
            this.w = w;
        }
    }

    static LinkedList<edge>[] adj;
    static long[] dis;
    static boolean[] vis;
    static int n, m;
    static final long INF = (long) 1e12;
    static int[] pre;

    static void Dijkstra(int start) {
        Arrays.fill(dis, INF);
        Arrays.fill(vis, false);
        class node {
            int u;
            long dis;

            public node(int u, long dis) {
                this.u = u;
                this.dis = dis;
            }
        }
        PriorityQueue<node> q = new PriorityQueue<>(((o1, o2) -> (int) (o1.dis - o2.dis)));
        dis[start] = 0;
        q.add(new node(start, 0));
        while (!q.isEmpty()) {
            int u = q.poll().u;
            if (vis[u]) continue;
            vis[u] = true;
            for (edge e : adj[u])
                if (dis[e.v] > dis[u] + e.w) {
                    dis[e.v] = dis[u] + e.w;
                    q.add(new node(e.v, dis[e.v]));
                    pre[e.v] = u;
                }
        }
    }

    static String _findPathTool(int now) {
        String ans = "";
        //如果还有前驱节点
        if (pre[now] != 0) ans += _findPathTool(pre[now]);
        ans += now + "->";
        return ans;
    }

    static String findPath(int start, int end) {
        if (start == end) return "自身到自身";
        if (pre[end] == 0) return "无最短路";
        return _findPathTool(pre[end]) + end;
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        adj = new LinkedList[n + 1];
        for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
        dis = new long[n + 1];
        vis = new boolean[n + 1];
        pre = new int[n + 1];
        int start = sc.nextInt();
        for (int i = 0; i < m; ++i) {
            int u = sc.nextInt(), v = sc.nextInt();
            long w = sc.nextLong();
            adj[u].add(new edge(v, w));
        }
        Dijkstra(start);
        for (int i = 1; i <= n; ++i)
            System.out.printf("%d到%d的最短路径长度为%d,最短路径为:%s\n", start, i, dis[i], findPath(start, i));
    }
}

测试数据:

输入:
4 6 1
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
11的最短路径长度为0,最短路径为:自身到自身
12的最短路径长度为2,最短路径为:1->2
13的最短路径长度为4,最短路径为:1->2->3
14的最短路径长度为3,最短路径为:1->2->4

例题 Code

实现 1

import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;

public class Main {
    static class edge {
        int v;
        int w;

        public edge(int v, int w) {
            this.v = v;
            this.w = w;
        }
    }

    static LinkedList<edge>[] adj;
    static int[] dis;
    static boolean[] vis;
    static int n, m;
    static final int INF = Integer.MAX_VALUE;

    static void Dijkstra(int start) {
        Arrays.fill(dis, INF);
        Arrays.fill(vis, false);
        //自己到自己的最短路为0
        dis[start] = 0;
        for (int i = 1; i <= n; ++i) {
            //操作1:在未确定的集合T中选择最短路长度最小的结点
            int u = 0;
            long min = INF;
            for (int j = 1; j <= n; ++j)
                if (!vis[j] && dis[j] < min) {
                    u = j;
                    min = dis[j];
                }
            //如果u为初始值,则说明所有节点都被选择了
            if (u == -1) return;
            //将其加入集合S中
            vis[u] = true;
            //操作2:对加入的结点的所有出边进行松弛操作
            for (edge e : adj[u])
                //松弛操作
                if (dis[e.v] > (long) dis[u] + e.w)
                    dis[e.v] = dis[u] + e.w;
        }
    }

    static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
    static PrintWriter out = new PrintWriter(System.out);

    static int get() throws IOException {
        in.nextToken();
        return (int) in.nval;
    }

    public static void main(String[] args) throws IOException {
        n = get();
        m = get();
        //点的标号从1开始,即1~n
        adj = new LinkedList[n + 1];
        for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
        dis = new int[n + 1];
        vis = new boolean[n + 1];
        // start为源点
        int start = get();
        for (int i = 0; i < m; ++i) {
            int u = get(), v = get(), w = get();
            // 加边(有向边)
            adj[u].add(new edge(v, w));
        }
        Dijkstra(start);
        for (int i = 1; i <= n; ++i) out.print(dis[i] + " ");
        out.close();
    }
}

实现 2

import java.io.*;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.PriorityQueue;

public class Main {
    static class edge {
        int v, w;

        public edge(int v, int w) {
            this.v = v;
            this.w = w;
        }
    }

    static LinkedList<edge>[] adj;
    static int[] dis;
    static boolean[] vis;
    static int n, m;
    static final int INF = Integer.MAX_VALUE;

    static void Dijkstra(int start) {
        Arrays.fill(dis, INF);
        Arrays.fill(vis, false);
        class node {
            int u, dis;

            public node(int u, int dis) {
                this.u = u;
                this.dis = dis;
            }
        }
        PriorityQueue<node> q = new PriorityQueue<>(((o1, o2) -> o1.dis - o2.dis));
        //自己到自己的最短路为0
        dis[start] = 0;
        q.add(new node(start, 0));
        while (!q.isEmpty()) {
            //操作1:选择最短路长度最小的结点
            int u = q.poll().u;
            //如果已经在集合中,则选择下一个节点
            if (vis[u]) continue;
            vis[u] = true;
            //操作2:对加入的结点的所有出边进行松弛操作
            for (edge e : adj[u])
                //松弛操作
                if (dis[e.v] > (long) dis[u] + e.w) {
                    dis[e.v] = dis[u] + e.w;
                    q.add(new node(e.v, dis[e.v]));
                }
        }
    }

    static StreamTokenizer in = new StreamTokenizer(new BufferedReader(new InputStreamReader(System.in)));
    static PrintWriter out = new PrintWriter(System.out);

    static int get() throws IOException {
        in.nextToken();
        return (int) in.nval;
    }

    public static void main(String[] args) throws IOException {
        n = get();
        m = get();
        //点的标号从1开始,即1~n
        adj = new LinkedList[n + 1];
        for (int i = 1; i <= n; ++i) adj[i] = new LinkedList<>();
        dis = new int[n + 1];
        vis = new boolean[n + 1];
        // start为源点
        int start = get();
        for (int i = 0; i < m; ++i) {
            int u = get(), v = get(), w = get();
            // 加边(有向边)
            adj[u].add(new edge(v, w));
        }
        Dijkstra(start);
        for (int i = 1; i <= n; ++i) out.print(dis[i] + " ");
        out.close();
    }
}

全源最短路

例题:P2910 Clear And Present Danger S - 洛谷

全源最短路可以通过单源最短路求得,即对任意两个点求单源最短路。

但有更简单的方法

Floyd

forforfor 求最短路

  • 是用来求任意两个结点之间的最短路的。

  • 复杂度比较高,但是常数小,容易实现。

  • 适用于任何图,不管有向无向,边权正负,但是最短路必须存在。(不能有负环)

  • 是一种动态规划算法,也称插点法(每次在两个点的路径间新增点)。

过程

定义 dis(k,i,j) 表示 由 ij ,且 只允许经过节点编号为 1k 的最短路径长度

对于 dis(k,i,j) 路径的选择分为两类:

  1. 路径不经过 k 点,则 dis(k,i,j)=dis(k1,i,j)
  2. 路径经过 k 点,则 dis(k,i,j)=dis(k1,i,k)+dis(k1,k,j)

因此 dis(k,i,j)=min(dis(k1,i,j), dis(k1,i,k)+dis(k1,k,j))

注意,当计算第 k层的 dis(k,i,j) 时,必须先将前 k1 层的所有状态计算出来,因此循环时 k 应在最外层

初始化

  1. i=j 时,由于自身到自身的最短路径长度为 0,则 dis(0,i,j)=0
  2. ij 且存在 ij 直接相连的边时,dis(0,i,j)=w(i,j),其中 w(i,j)ij 的边权
  3. ij 且存在 ij 直接相连的边时,dis(0,i,j)=+

优化

重看状态计算:

  1. 当路径不经过 k 点时,相当于直接将 k1 层的值复制到 k
  2. 当路径经过 k 点时,第 k1 层的 dis(k1,i,k)dis(k1,k,j) 都不经过 k 点,可以直接投影到第 k 层的对应位置,即 dis(i,j)=dis(i,k)+dis(k,j)

因此,dis(i,j)=min(dis(i,j), dis(i,k)+dis(k,j))

性质

  1. 每新增一个点,就等于新增一个桥,尝试更新那些经过新桥的最短路径
  2. 当循环完 k 层后,会将有插点编号不超过 k 的最短路径都找出来,但是插点编号超过 k 的最短路径是找不出来的
  3. 当枚举完 n 层,所有的最短路径都找出来了
  4. Floyd 算法运行后,若 dis(i,j) 为初值 +,则无最短路径,即他们不可达

时间复杂度

显然,时间复杂度为 O(n3),空间复杂度优化前为 O(n3),优化后为 O(n2)

实现

Floyd 算法不需要遍历图,因此可以选择不存图

import java.util.Scanner;

public class Main {

    static int[][] dis;
    static int n, m;
    static final int INF = (int) 1e9;

    static void Floyd() {
        for (int k = 1; k <= n; ++k)
            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= n; ++j)
                    //dis[i][j] = Math.min(dis[i][j], dis[i][k] + dis[k][j]);
                    if (dis[i][j] > dis[i][k] + dis[k][j]) dis[i][j] = dis[i][k] + dis[k][j];
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        //点的标号从1开始,即1~n
        dis = new int[n + 1][n + 1];
        //初始化
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j < i; ++j) dis[i][j] = dis[j][i] = INF;
            dis[i][i] = 0;
        }
        for (int i = 0; i < m; ++i) {
            //有向边
            int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
            dis[u][v] = w;
        }
        Floyd();
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j) {
                if (dis[i][j] == INF) System.out.printf("%d不可达到%d\n", i, j);
                else System.out.printf("%d到%d的最短路径长度为%d\n", i, j, dis[i][j]);
            }
    }
}

测试数据:

输入:
4 6
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
11的最短路径长度为0
12的最短路径长度为2
13的最短路径长度为4
14的最短路径长度为3
2不可达到1
22的最短路径长度为0
23的最短路径长度为2
24的最短路径长度为1
3不可达到1
3不可达到2
33的最短路径长度为0
34的最短路径长度为3
4不可达到1
4不可达到2
4不可达到3
44的最短路径长度为0

输出路径

每次更新最短路径时,记录插点,即记录这次结果是从哪个中转点过来的

import java.util.Scanner;

public class Main {

    static int[][] dis;
    static int[][] path;
    static int n, m;
    static final int INF = (int) 1e9;

    static void Floyd() {
        for (int k = 1; k <= n; ++k)
            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= n; ++j)
                    if (dis[i][j] > dis[i][k] + dis[k][j]) {
                        dis[i][j] = dis[i][k] + dis[k][j];
                        path[i][j] = k;
                    }
    }

    static String _findPathTool(int x, int y) {
        String ans = "";
        int k = path[x][y];
        // 如果 x 到 k 有插点
        if (path[x][k] != 0) ans += _findPathTool(x, k);
        // 中转点
        ans += k + "->";
        // 如果 k 到 y 有插点
        if (path[k][y] != 0) ans += _findPathTool(k, y);
        return ans;
    }

    static String findPath(int x, int y) {
        if (x == y) return "自身到自身";
        // 如果x到y间没有插点
        if (path[x][y] == 0) return x + "->" + y;
        return x + "->" + _findPathTool(x, y) + y;

    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        //点的标号从1开始,即1~n
        dis = new int[n + 1][n + 1];
        path = new int[n + 1][n + 1];
        //初始化
        for (int i = 1; i <= n; ++i) {
            for (int j = 1; j < i; ++j) dis[i][j] = dis[j][i] = INF;
            dis[i][i] = 0;
        }
        for (int i = 0; i < m; ++i) {
            //有向边
            int u = sc.nextInt(), v = sc.nextInt(), w = sc.nextInt();
            dis[u][v] = w;
        }
        Floyd();
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j) {
                if (dis[i][j] == INF) System.out.printf("%d不可达到%d\n", i, j);
                else System.out.printf("%d到%d的最短路径长度为%d,最短路径为:%s\n", i, j, dis[i][j], findPath(i, j));
            }
    }
}

测试数据:

输入:
4 6
1 2 2
2 3 2
2 4 1
1 3 5
3 4 3
1 4 4
输出:
11的最短路径长度为0,最短路径为:自身到自身
12的最短路径长度为2,最短路径为:1->2
13的最短路径长度为4,最短路径为:1->2->3
14的最短路径长度为3,最短路径为:1->2->4
2不可达到1
22的最短路径长度为0,最短路径为:自身到自身
23的最短路径长度为2,最短路径为:2->3
24的最短路径长度为1,最短路径为:2->4
3不可达到1
3不可达到2
33的最短路径长度为0,最短路径为:自身到自身
34的最短路径长度为3,最短路径为:3->4
4不可达到1
4不可达到2
4不可达到3
44的最短路径长度为0,最短路径为:自身到自身

例题 Code

import java.util.Scanner;

public class Main {

    static int[][] dis;
    static int[] path;
    static int n, m, ans = 0;

    static void Floyd() {
        for (int k = 1; k <= n; ++k)
            for (int i = 1; i <= n; ++i)
                for (int j = 1; j <= n; ++j)
                    if (dis[i][j] > dis[i][k] + dis[k][j])
                        dis[i][j] = dis[i][k] + dis[k][j];
    }


    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);
        n = sc.nextInt();
        m = sc.nextInt();
        //点的标号从1开始,即1~n
        dis = new int[n + 1][n + 1];
        path = new int[m + 1];
        for (int i = 1; i <= m; ++i) path[i] = sc.nextInt();
        for (int i = 1; i <= n; ++i)
            for (int j = 1; j <= n; ++j)
                dis[i][j] = sc.nextInt();
        Floyd();
        for (int i = 2; i <= m; ++i) ans += dis[path[i - 1]][path[i]];
        System.out.println(ans);
    }
}

参考资料

最短路 - OI Wiki

303 最短路 Floyd 算法 - 董晓算法 _哔哩哔哩

posted @   Cattle_Horse  阅读(72)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示