一、单元最短路径算法
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求最短路径
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)); } }