路径算法Dijistra,Floyd,Bellman-ford,spfa

一、单元最短路径算法

1. 存图方式:

邻接矩阵:适用于边数较多的稠密图使用,

邻接表:适用于边数较少的稀疏图使用,当边数量接近点的数量, m=n

类:

class Edge {
    int start, end, weight;
​
    Edge(int _a, int _b, int _c) {
        start = _a;
        end = _b;
        weight = _c;
    }
}

2. Dijistra算法

2.1 朴素版

/**
 * 贪心  O N^2
 * 1.找距离最短的节点 2.计算该节点扩展的边
 *
 * 源节点加入最短路径集合,先算出这个节点的周围路径长度。  继续加入 不属于这个集合的且到这个集合 路径最短的节点,再算边
 * 注意:graph是根据下标两点之间的值,如果到达不了则设置为Integer.MAXVALUE,不一定是lc题目那种
 *
 * P743网络延迟时间   https://leetcode.cn/problems/network-delay-time/solution/gong-shui-san-xie-yi-ti-wu-jie-wu-chong-oghpz/
 * 存图方式
 * 在开始讲解最短路之前,我们先来学习三种「存图」方式。
 *
 * 邻接矩阵
 * 这是一种使用二维矩阵来进行存图的方式。
 *
 * 适用于边数较多的稠密图使用,当边数量接近点的数量的平方
 */
public class Dijkstra {
​
    public int[] dijkstra(int[][] graph, int n, int k) {
        int INF = Integer.MAX_VALUE;
        // 源节点到其他节点的最短距离
        int[] dist = new int[n];
        Arrays.fill(dist, INF);
        // 源节点到自身距离为0
        dist[k] = 0;
​
        // 最短路径节点集合
        boolean[] visited = new boolean[n];
​
        // 1.找距离最短的节点 2.计算该节点扩展的边
        // n -1  也行,是最后一轮循环确定倒数第二个节点时,也确定了到最后一个节点的边距离,可以省一轮遍历
        for (int i = 0; i < n; ++i) {
            int x = findNextNode(dist, visited, n);
            // 源节点无法到达任务一个点
            if (x == -1) {
                return dist;
            }
            visited[x] = true;
            for (int y = 0; y < n; ++y) {
                // 根据这个节点计算 最短路径数组 不通的边不考虑
                if (graph[x][y] != Integer.MAX_VALUE && dist[x] + graph[x][y] < dist[y]) {
                    dist[y] = dist[x] + graph[x][y];
                }
            }
        }
​
        return dist;
    }
​
    // 首次默认是源节点,找到距离最短的节点 这里是遍历所有的点到该点距离
    private int findNextNode(int[] dist, boolean[] visit, int n) {
        int u = -1;
        int min = Integer.MAX_VALUE;
        for (int j = 0; j < n; j++) {
            if (!visit[j] && dist[j] < min) {
                min = dist[j];
                u = j;
            }
        }
        return u;
    }
​
    // 下面也可以这样找最短节点  代替 findNextNode(),但是不通的节点也会加入计算,多余了点步骤
    //            int x = -1;
    //            for (int y = 0; y < n; ++y) {
    //                if (!visited[y] && (x == -1 || dist[y] < dist[x])) {
    //                    x = y;
    //                }
    //            }
}

2.2 堆优化版

通过堆替代 所有边找最短结点

/**
 * 堆优化 - Dijkstra  O(mlogn+n)
 * 邻接表(数组)
 * 这也是一种在图论中十分常见的存图方式,与数组存储单链表的实现一致(头插法)。
 *
 * 这种存图方式又叫链式前向星存图。
 *
 * 适用于边数较少的稀疏图使用,当边数量接近点的数量, m=n
 * 下标0开始
 */
public class DijistraHeapMy {
    // 头节点指向的边
    private int[] srcToEdge;
​
    // 边指向节点
    private int[] edgeToDist;
​
    // 边的下一条边,同一个出发节点src   简称邻接边
    private int[] nextEdge;
​
    // 边的权重
    private int[] w;
​
    private int[] dist;
​
    private boolean[] visited;
​
    private int idx;
​
    private int INF = Integer.MAX_VALUE;
​
    private void add(int srcV, int distV, int weight) {
        this.edgeToDist[idx] = distV;
        this.nextEdge[idx] = this.srcToEdge[srcV];
        this.srcToEdge[srcV] = idx;
        this.w[idx] = weight;
        idx++;
    }
​
    private void init(int n) {
        visited = new boolean[n];
        dist = new int[n];
        w = new int[6000];
        nextEdge = new int[6000]; // 题目要求的边最多6000
        srcToEdge = new int[n]; // 头节点数量
        edgeToDist = new int[6000];
        Arrays.fill(srcToEdge, -1);
        Arrays.fill(nextEdge, -1);
        Arrays.fill(edgeToDist, -1);
        Arrays.fill(dist, INF);
        Arrays.fill(visited, false);
    }
​
    // k:源节点的下标,从0开始
    public int[] dijkstra(int[][] times, int n, int k) {
        // 初始化数组
        init(n);
        for (int[] ts : times) {
            // 从下标0开始
            add(ts[0] - 1, ts[1] - 1, ts[2]);
        }
​
        PriorityQueue<int[]> queue = new PriorityQueue<>((a, b) -> a[1] - b[1]);
        queue.add(new int[]{k, 0});
        dist[k] = 0;
​
        while (!queue.isEmpty()) {
            // 确定点
            int v = queue.poll()[0];
            if (visited[v]) {
                continue;
            }
            visited[v] = true;
​
            // 确定最短集合边
            for (int i = srcToEdge[v]; i != -1; i = nextEdge[i]) {
                int distNode = edgeToDist[i];
​
                System.out.println("结点" + v + " -> 结点" + distNode);
                System.out.println("编号为" + idx + "的边, 权重为: " + w[idx]);
​
                // 由于是通过边数组确定的,所以这里不用判断 !=INF 因为都是能通的边
                if (w[i] + dist[v] < dist[distNode]) {
                    dist[distNode] = w[i] + dist[v];
                    queue.add(new int[]{distNode, dist[distNode]});
                }
            }
        }
        return dist;
    }
}

3. Floyd算法

/**
 * 动态规划 - 邻接矩阵    O N^3
 *
 * 跑一遍 Floyd,可以得到「从任意起点出发,到达任意起点的最短距离   时间复杂度On 3 > Dijkstra On 2
 *
 * P743网络延迟时间Floyd
 *
 * 通过循环每个中转点求路径
 */
public class Floyd {
    private static int INF = Integer.MAX_VALUE;
​
    /**
     * 距离矩阵   自身为0,到达不了为INF
     */
    public static int[][] distance;
    /**
     * 路径矩阵
     */
    public static int[][] path; // 一些求最短距离的点不需要记录路径矩阵也行
​
    public static void floyd(int[][] graph) {
        //初始化距离矩阵 distance
        distance = graph;
        //初始化路径
        path = new int[graph.length][graph.length];
        for (int i = 0; i < graph.length; i++) {
            for (int j = 0; j < graph[i].length; j++) {
                path[i][j] = j;
            }
        }
        //开始 Floyd 算法
        //每个点为中转
        for (int i = 0; i < graph.length; i++) {
            //所有入度
            for (int j = 0; j < graph.length; j++) {
                //所有出度
                for (int k = 0; k < graph[j].length; k++) {
                    //以每个点为「中转」,刷新所有出度和入度之间的距离
                    //例如 AB + BC < AC 就刷新距离
                    if (graph[j][i] != INF && graph[i][k] != INF) {
                        // 如果两点到达不了也只能是 走中转节点
                        if (graph[j][i] + graph[i][k] < graph[j][k] || graph[j][k] == INF) {
                            //刷新距离
                            graph[j][k] = graph[j][i] + graph[i][k];
                            //刷新路径
                            path[j][k] = i;
                        }
                    }
                }
            }
        }
    }
​
    /**
     * 测试
     */
    public static void main(String[] args) {
//        int[][] graph = new int[][]{
//            {0, 2, INF, 6}
//            , {2, 0, 3, 2}
//            , {INF, 3, 0, 2}
//            , {6, 2, 2, 0}};
        // 以下是转换后的 graph
        int[][] graph = new int[][]{
            {0, 1, INF, INF}
            , {INF, 0, 1, INF}
            , {INF, INF, 0, 1}
            , {INF, INF, INF, 0}};
​
        floyd(graph);
        System.out.println("====distance====");
        // distance根据graph 修改为最短路径数组
        for (int[] ints : distance) {
            for (int anInt : ints) {
                System.out.print(anInt + " ");
            }
            System.out.println();
        }
        System.out.println("====path====");
        for (int[] ints : path) {
            for (int anInt : ints) {
                System.out.print(anInt + " ");
            }
            System.out.println();
        }
    }
}

4. Bellman-ford算法

理解 Bellman-ford算法_bellmanford算法_十七溯的博客-CSDN博客

防止串联原理 Bellman-ford算法详解_bellmanford算法_真的没事鸭的博客-CSDN博客

核心代码

    //经过 n - 1次松驰
    //对所有边进行一次松弛操作,就求出了源点到所有点,经过的边数最多为1的最短路
    //对所有边进行两次松弛操作,就求出了源点到所有点,经过的边数最多为2的最短路
    //....
    //对所有边进行n- 1次松弛操作,就求出了源点到所有点,经过的边数最多为n - 1的最短路
    for(int i = 0; i < n - 1; i++) {
        
        //遍历所有边
        for(int j = 0; j < m; j++) {
            int a = edges[j].a, b = edges[j].b, w = edges[j].w;
            if(dis[a] != 0x3f3f3f3f && dis[b] > dis[a] + w) //松弛操作
                dis[b] = dis[a] + w;
        }
    }

5. SPFA算法

5.1 SPFA求最短路径

SPFA算法(最短路径算法)_钟一淼的博客-CSDN博客

1.用dis数组记录点到有向图的任意一点距离,初始化起点距离为0,其余点均为INF,起点入队。

2.判断该点是否存在。(未存在就入队,标记)

3.队首出队,并将该点标记为没有访问过,方便下次入队。

4.遍历以对首为起点的有向边(t,i),如果dis[i]>dis[t]+w(t,i),则更新dis[i]。

5.如果i不在队列中,则入队标记,一直到循环为空。

/**
 * 对 BellmanFord 的优化  求最短路径   O M*N
 * 能够解决负环问题: 负环,又叫负权回路,负权环,指的是一个图中存在一个环,里面包含的边的边权总和<0
 * <p>
 * 与Dikstra类似,D是类似DFS找最短距离的点深入,SPFA是类似BFS每次从相邻节点扩展,而且访问过可以重复访问,可以解决负环问题
 * <p>
 * https://blog.csdn.net/m0_64045085/article/details/123547253
 */
public class SpfaMy {
    // 边到节点
    private int[] edgeToNode;
​
    // 邻接边
    private int[] nextEdge;
​
    // 结点到边
    private int[] srcToEdge;
​
    // 边权重
    private int[] w;
​
    // 第几条边,从0开始
    private int idx;
​
    private boolean[] visited;
​
    private int INF = Integer.MAX_VALUE;
​
    // 这里 k times 从下标1开始
    public int[] spfa(int[][] times, int n, int k) {
        // 初始化最短距离
        int[] dist = new int[n + 1];
        Arrays.fill(dist, INF);
        dist[k] = 0;
​
        // 初始化是否访问过 由于下表从1开始所以n+1
        visited = new boolean[n + 1];
        Arrays.fill(visited, false);
        visited[k] = true;
​
        // 初始化表
        edgeToNode = new int[6000]; // 题目最多6000条边
        nextEdge = new int[6000];
        srcToEdge = new int[n + 1]; // 由于下表从1开始所以n+1
        w = new int[6000];
​
        // 初始化邻接表
        Arrays.fill(srcToEdge, -1);
        Arrays.fill(nextEdge, -1);
        for (int[] ts : times) {
            edgeToNode[idx] = ts[1];
            nextEdge[idx] = srcToEdge[ts[0]];
            srcToEdge[ts[0]] = idx;
            w[idx] = ts[2];
            idx++;
        }
​
        // 初始化队列
        Deque<Integer> queue = new ArrayDeque<>();
        queue.add(k);
​
        while (!queue.isEmpty()) {
            System.out.print("队列此时为" + queue);
            Integer poll = queue.poll();
            visited[poll] = false; // 这是能够探测负权的操作,如果原结点的距离更短,则最短路径继续能探测原结点
            System.out.println(",更新" + poll + "结点");
​
            // 循环点的邻接边  i是边  j是结点
            for (int i = srcToEdge[poll]; i != -1; i = nextEdge[i]) {
                int j = edgeToNode[i];
                if (dist[poll] + w[i] < dist[j]) {
                    dist[j] = dist[poll] + w[i];
                    System.out.println("计算 " + poll + " 至 " + j + " ,到 " + j + " 最短距离为 " + dist[j]);
​
                    if (!visited[j]) {
                        queue.add(j);
                        visited[j] = true;
                    }
                }
            }
        }
        return dist;
    }
}

5.2 SPFA求负环

852. spfa判断负环_51CTO博客_spfa判断负环

负环,又叫负权回路,负权环,指的是一个图中存在一个环,里面包含的边的边权总和<0

(1)(spfa)可以用来判断是不是有向图中存在负环。

(2)基本原理:利用抽屉原理

基于抽屉原理,如果一条正在搜索的最短路径上的点的个数大于总共点的个数,则说明路径上一定有至少重复的两个点,走了回头路。

(dist[x])的概念是指当前从(1)号点到(x)号点的最短路径的长度。(dist[x]=dist[t]+w[i]) (cnt[x])的概念是指当前从(1)号点到(x)号点的最短路径的边数量。(cnt[x]=cnt[t]+1) 如果发现(cnt[x]>=n),就意味着从(1)(x)经历了(n)条边,那么必须经过了(n+1)个点,但问题是点一共只有(n)个,所以必然有两个点是相同的,就是有一个环。 因为是在不断求最短路径的过程中发现了环,路径长度在不断变小的情况下发现了环,那么,只能是负环。 (1)号点是源节点的意思

(3)为什么初始化时初始值为(0),而且把所有结点都加入队列?

在原图的基础上新建一个虚拟源点,从该点向其他所有点连一条权值为(0)的有向边。那么原图有负环等价于新图有负环。此时在新图上做(spfa),将虚拟源点加入队列中。然后进行(spfa)的第一次迭代,这时会将所有点的距离更新并将所有点插入队列中。执行到这一步,就等价于下面代码中的做法了。如果新图有负环,等价于原图有负环。

/**
 * 对 BellmanFord 的优化  判断负环   O M*N
 * 能够解决负环问题: 负环,又叫负权回路,负权环,指的是一个图中存在一个环,里面包含的边的边权总和<0
 * https://blog.51cto.com/u_3044148/4028313
 * <p>
 * 抽屉原理,如果一条正在搜索的[最短路径]上的点的个数大于总共点的个数,则说明路径上一定有至少重复的两个点,走了回头路
 * 某个点的入队数大于了n,证明他在不停得松弛
 */
public class SpfaMyNegativeRing {
    class Edge {
        private int start;
​
        private int end;
​
        private int w;
​
        public Edge(int a, int b, int c) {
            this.start = a;
            this.end = b;
            this.w = c;
        }
    }
​
    private List<Edge> es = new LinkedList<>();
​
    // 这里times 从下标0开始
    public boolean spfa(int[][] times, int n) {
        // 初始化队列
        Deque<Integer> queue = new ArrayDeque<>();
        for (int i = 0; i < n; i++) {
            // 把所有点全部放入到队列中,因为我们不是要找从1点出发的负环,而是要找整个图中的负环
            // 每个点都相当于虚拟源点,只要从这个点出发的最短路径上,某个点入队超过N,就是有负环
            queue.add(i);
        }
​
        for (int[] ts : times) {
            es.add(new Edge(ts[0] - 1, ts[1] - 1, ts[2]));
        }
​
        // 都设置为0,只有负数的情况下才会是最短路径,没有负数就没有负环
        int[] dist = new int[n];
​
        // 源点到下标点的最短路径的边数量
        int[] cnt = new int[n];
​
        boolean[] visited = new boolean[n];
​
        while (!queue.isEmpty()) {
            int poll = queue.poll();
            visited[poll] = false;
​
            // 此处不是邻接边,是所有边了
            for (Edge edge : es) {
                // 由于所有的最短路径 dist都是0,所以只有负数的才会是最短路径
                if (dist[poll] + edge.w < dist[edge.end]) {
                    dist[edge.end] = dist[poll] + edge.w;
​
                    // 到达的点入队的数标为前面这个点入队的次数+1 , 假设循环是好几个点负边连起来的就明白了,  1->2->3>1  权值都为负数
                    cnt[edge.end] = cnt[poll] + 1;
                    if (cnt[edge.end] >= n) {
                        return false;
                    }
​
                    if (!visited[edge.end]) {
                        queue.add(edge.end);
                        visited[edge.end] = true;
                    }
                }
            }
        }
        return true;
    }
​
    public static void main(String[] args) {
        System.out.println(new SpfaMyNegativeRing().spfa(new int[][]{{1, 2, 1}, {2, 3, 2}, {1, 3, 5}, {3, 4, 1},
            {2, 1, -1}}, 4));
    }
}

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值