定义

最短路问题的定义为:设 \(G=(V,E)\) 为连通图,图中各边 \((v_i,v_j)\) 有权 \(l_{ij}\) (\(l_{ij}=\infty\) 表示 \(v_i,v_j\) 间没有边) ,\(v_s,v_t\) 为图中任意两点,求一条道路 \(\mu\),使得它是从 \(v_s\) 到 \(v_t\) 的所有路中总权最小的路,即:\(L(\mu)=\sum_{(v_i,v_j)\in \mu}l_{ij}\) 最小。

下图左侧是一幅带权有向图,以顶点 0 为起点到各个顶点的最短路径形成的最短路径树如下图右侧所示:

带权有向图的实现

在实现最短路算法之前需要先实现带权有向图。在上一篇博客 《如何在 Java 中实现最小生成树算法》 中我们实现了带权无向图,只需一点修改就能实现带权有向图。

带权有向边

首先应该实现带权有向图中的边 DirectedEdge,这个类有三个成员变量:指出边的顶点 v、边指向的顶点 w 和边的权重 weight。代码如下所示:

  1. package com.zhiyiyo.graph;
  2. /**
  3. * 带权有向边
  4. */
  5. public class DirectedEdge {
  6. int v, w;
  7. double weight;
  8. public DirectedEdge(int v, int w, double weight) {
  9. this.v = v;
  10. this.w = w;
  11. this.weight = weight;
  12. }
  13. public int from() {
  14. return v;
  15. }
  16. public int to() {
  17. return w;
  18. }
  19. public double getWeight() {
  20. return weight;
  21. }
  22. @Override
  23. public String toString() {
  24. return String.format("%d->%d(%.2f)", v, w, weight);
  25. }
  26. }

带权有向图

带权有向图的实现非常简单,只需将带权无向图使用的 Edge 类换成 DirectedEdge 类,并作出少许调整即可:

  1. package com.zhiyiyo.graph;
  2. import com.zhiyiyo.collection.stack.LinkStack;
  3. import com.zhiyiyo.collection.stack.Stack;
  4. public class WeightedDigraph {
  5. private final int V;
  6. protected int E;
  7. protected LinkStack<DirectedEdge>[] adj;
  8. public WeightedDigraph(int V) {
  9. this.V = V;
  10. adj = (LinkStack<DirectedEdge>[]) new LinkStack[V];
  11. for (int i = 0; i < V; i++) {
  12. adj[i] = new LinkStack<>();
  13. }
  14. }
  15. public int V() {
  16. return V;
  17. }
  18. public int E() {
  19. return E;
  20. }
  21. public void addEdge(DirectedEdge edge) {
  22. adj[edge.from()].push(edge);
  23. E++;
  24. }
  25. public Iterable<DirectedEdge> adj(int v) {
  26. return adj[v];
  27. }
  28. public Iterable<DirectedEdge> edges() {
  29. Stack<DirectedEdge> edges = new LinkStack<>();
  30. for (int v = 0; v < V; ++v) {
  31. for (DirectedEdge edge : adj(v)) {
  32. edges.push(edge);
  33. }
  34. }
  35. return edges;
  36. }
  37. }

最短路算法

API

最短路算法应该支持起始点 \(v_s\) 到任意顶点 \(v_t\) 的最短距离和最短路径的查询:

  1. package com.zhiyiyo.graph;
  2. /**
  3. * 最短路径
  4. */
  5. public interface ShortestPath {
  6. /**
  7. * 从起点到顶点 v 的最短距离,如果顶点 v 不可达则为无穷大
  8. * @param v 顶点 v
  9. * @return 最短路径
  10. */
  11. double distTo(int v);
  12. /**
  13. * 是否存在从起点到顶点 v 的路径
  14. * @param v 顶点 v
  15. * @return 是否存在
  16. */
  17. boolean hasPathTo(int v);
  18. /**
  19. * 从起点到顶点 v 的最短路径,若不存在则返回 null
  20. * @param v 顶点 v
  21. * @return 最短路径
  22. */
  23. Iterable<DirectedEdge> pathTo(int v);
  24. }

Dijkstra 算法

我们可以使用一个距离数组 distTo[] 来保存起始点 \(v_s\) 到其余顶点 \(v_t\) 的最短路径,且 distTo[] 数组满足以下条件:

\[distTo(t) = \left\{ \begin{aligned} 0 \quad & t=s \\ l_{st} \quad & t\neq s 且\ t\ 可达\\ \infty \quad & t\ 不可达 \end{aligned} \right. \]

可以使用 Double.POSITIVE_INFINITY 来表示无穷大,有了 distTo[] 之后就能实现 ShortestPath 前两个方法:

  1. package com.zhiyiyo.graph;
  2. public class DijkstraSP implements ShortestPath {
  3. private double[] distTo;
  4. @Override
  5. public double distTo(int v) {
  6. return distTo[v];
  7. }
  8. @Override
  9. public boolean hasPathTo(int v) {
  10. return distTo[v] < Double.POSITIVE_INFINITY;
  11. }
  12. }

为了保存 \(v_s\) 到 \(v_t\) 的最短路径,可以使用一个边数组 edgeTo[],其中 edgeTo[v] = e_wv 表示要想到达 \(v_t\),需要先经过顶点 \(v_w\),接着从 edgeTo[w]获取到达 \(v_w\) 之前需要到达的上一个节点,重复上述步骤直到发现 edgeTo[i] = null,这时候就说明我们回到了 \(v_s\)。 获取最短路径的代码如下所示:

  1. @Override
  2. public Iterable<DirectedEdge> pathTo(int v) {
  3. if (!hasPathTo(v)) return null;
  4. Stack<DirectedEdge> path = new LinkStack<>();
  5. for (DirectedEdge e = edgeTo[v]; e != null; e = edgeTo[e.from()]) {
  6. path.push(e);
  7. }
  8. return path;
  9. }

算法流程

虽然我们已经实现了上述接口,但是如何得到 distTo[]edgeTo[] 还是个问题,这就需要用到 Dijkstra 算法了。算法的思想是这样的:

  1. 初始化 distTo[] 使得除了 distTo[s] = 0 外,其余的元素都为 Double.POSITIVE_INFINITY。同时初始化 edgeTo[] 的每个元素都是 null

  2. 将顶点 s 的所有相邻顶点 \(v_j\) 加入集合 \(V'\) 中,设置 distTo[j] = l_sj 即初始化最短距离为邻边的权重;

  3. 从 \(V'\) 中取出距离最短即 distTo[m] 最小的顶点 \(v_m\),遍历 \(v_m\) 的所有邻边 \((v_m, v_w)\),如果有 \(l_{mw}+l_{sm}<l_{sw}\),就说明从 \(v_s\) 走到 \(v_m\) 再一步走到 \(v_w\) 距离最短,我们就去更新 distTo[m],同时将 \(v_w\) 添加到 \(V'\) 中(如果 \(v_w\) 不在的话);

  4. 重复上述过程直到 \(V'\) 变为空,我们就已经找到了所有 \(v_s\) 可达的顶点的最短路径。

上述过程中有个地方会影响算法的性能,就是如何从 \(V'\) 中取出最小距离对应的顶点 \(v_m\)。如果直接遍历 \(V'\) 最坏情况下时间复杂度为 \(O(|V|)\),如果换成最小索引优先队列则可以将时间复杂度降至 \(O(\log|V|)\)。

最小索引优先队列

上一篇博客 《如何在 Java 中实现最小生成树算法》 中介绍了最小堆的使用,最小堆可以在对数时间内取出数据集合中的最小值,对应到最短路算法中就是最短路径。但是有一个问题,就是我们想要的是最短路径对应的那个顶点 \(v_m\),只使用最小堆是做不到这一点的。如何能将最小堆中的距离值和顶点进行绑定呢?这就要用到索引优先队列。

索引优先队列的 API 如下所示,可以看到每个元素 item 都和一个索引 k 进行绑定,我们可以通过索引 k 读写优先队列中的元素。想象一下堆中的所有元素放在一个数组 pq 中,索引优先队列可以做到在对数时间内取出 pq 的最小值。

  1. package com.zhiyiyo.collection.queue;
  2. /**
  3. * 索引优先队列
  4. */
  5. public interface IndexPriorQueue<K extends Comparable<K>> {
  6. /**
  7. * 向堆中插入一个元素
  8. *
  9. * @param k 元素的索引
  10. * @param item 插入的元素
  11. */
  12. void insert(int k, K item);
  13. /**
  14. * 修改堆中指定索引的元素值
  15. * @param k 元素的索引
  16. * @param item 新的元素值
  17. */
  18. void change(int k, K item);
  19. /**
  20. * 向堆中插入或修改元素
  21. * @param k 元素的索引
  22. * @param item 新的元素值
  23. */
  24. void set(int k, K item);
  25. /**
  26. * 堆是否包含索引为 k 的元素
  27. * @param k 索引
  28. * @return 是否包含
  29. */
  30. boolean contains(int k);
  31. /**
  32. * 弹出堆顶的元素并返回其索引
  33. * @return 堆顶元素的索引
  34. */
  35. int pop();
  36. /**
  37. * 弹出堆中索引为 k 为元素
  38. * @param k 索引
  39. * @return 索引对应的元素
  40. */
  41. K delete(int k);
  42. /**
  43. * 获取堆中索引为 k 的元素,如果 k 不存在则返回 null
  44. * @param k 索引
  45. * @return 索引为 k 的元素
  46. */
  47. K get(int k);
  48. /**
  49. * 获取堆中的元素个数
  50. */
  51. int size();
  52. /**
  53. * 堆是否为空
  54. */
  55. boolean isEmpty();
  56. }

实现索引优先队列比优先队列麻烦一点,因为需要维护每个元素的索引。之前我们是将元素按照完全二叉树的存放顺序进行存储,现在可以换成索引,而元素只需根据索引值 k 放在数组 keys[k] 处即可。只有索引数组 indexes[] 和元素数组 keys[] 还不够,如果我们想实现 contains(int k) 方法,目前只能遍历一下 indexes[],看看 k 在不在里面,时间复杂度是 \(O(|V|)\)。何不多维护一个数组 nodeIndexes[],使得它满足下述关系:

\[\text{nodeIndexes}(k) = \left\{ \begin{aligned} d \quad & k \in \text{indexes} \\ -1 \quad & k \notin \text{indexes} \end{aligned} \right. \]

如果能在 nodeIndexes[k] 不是 -1,就说明索引 \(k\) 对应的元素存在与堆中,且索引 k 在 indexes[] 中的位置为 \(d\),即有下述等式成立:

\[\text{indexes}[\text{nodeIndexes}[k]] = k\\ \text{nodeIndexes}[\text{indexes}[d]] = d \]

有了这三个数组之后我们就可以实现最小索引优先队列了:

  1. package com.zhiyiyo.collection.queue;
  2. import java.util.Arrays;
  3. import java.util.NoSuchElementException;
  4. /**
  5. * 最小索引优先队列
  6. */
  7. public class IndexMinPriorQueue<K extends Comparable<K>> implements IndexPriorQueue<K> {
  8. private K[] keys; // 元素
  9. private int[] indexes; // 元素的索引,按照最小堆的顺序摆放
  10. private int[] nodeIndexes; // 元素的索引在完全二叉树中的编号
  11. private int N;
  12. public IndexMinPriorQueue(int maxSize) {
  13. keys = (K[]) new Comparable[maxSize + 1];
  14. indexes = new int[maxSize + 1];
  15. nodeIndexes = new int[maxSize + 1];
  16. Arrays.fill(nodeIndexes, -1);
  17. }
  18. @Override
  19. public void insert(int k, K item) {
  20. keys[k] = item;
  21. indexes[++N] = k;
  22. nodeIndexes[k] = N;
  23. swim(N);
  24. }
  25. @Override
  26. public void change(int k, K item) {
  27. validateIndex(k);
  28. keys[k] = item;
  29. swim(nodeIndexes[k]);
  30. sink(nodeIndexes[k]);
  31. }
  32. @Override
  33. public void set(int k, K item) {
  34. if (!contains(k)) {
  35. insert(k, item);
  36. } else {
  37. change(k, item);
  38. }
  39. }
  40. @Override
  41. public boolean contains(int k) {
  42. return nodeIndexes[k] != -1;
  43. }
  44. @Override
  45. public int pop() {
  46. int k = indexes[1];
  47. delete(k);
  48. return k;
  49. }
  50. @Override
  51. public K delete(int k) {
  52. validateIndex(k);
  53. K item = keys[k];
  54. // 交换之后 nodeIndexes[k] 发生变化,必须先保存为局部变量
  55. int nodeIndex = nodeIndexes[k];
  56. swap(nodeIndex, N--);
  57. // 必须有上浮的操作,交换后的元素可能比上面的元素更小
  58. swim(nodeIndex);
  59. sink(nodeIndex);
  60. keys[k] = null;
  61. nodeIndexes[k] = -1;
  62. return item;
  63. }
  64. @Override
  65. public K get(int k) {
  66. return contains(k) ? keys[k] : null;
  67. }
  68. public K min() {
  69. return keys[indexes[1]];
  70. }
  71. /**
  72. * 获取最小的元素对应的索引
  73. */
  74. public int minIndex() {
  75. return indexes[1];
  76. }
  77. @Override
  78. public int size() {
  79. return N;
  80. }
  81. @Override
  82. public boolean isEmpty() {
  83. return N == 0;
  84. }
  85. /**
  86. * 元素上浮
  87. *
  88. * @param k 元素的索引
  89. */
  90. private void swim(int k) {
  91. while (k > 1 && less(k, k / 2)) {
  92. swap(k, k / 2);
  93. k /= 2;
  94. }
  95. }
  96. /**
  97. * 元素下沉
  98. *
  99. * @param k 元素的索引
  100. */
  101. private void sink(int k) {
  102. while (2 * k <= N) {
  103. int j = 2 * k;
  104. // 检查是否有两个子节点
  105. if (j < N && less(j + 1, j)) j++;
  106. if (less(k, j)) break;
  107. swap(k, j);
  108. k = j;
  109. }
  110. }
  111. /**
  112. * 交换完全二叉树中编号为 a 和 b 的节点
  113. *
  114. * @param a 索引 a
  115. * @param b 索引 b
  116. */
  117. private void swap(int a, int b) {
  118. int k1 = indexes[a], k2 = indexes[b];
  119. nodeIndexes[k2] = a;
  120. nodeIndexes[k1] = b;
  121. indexes[a] = k2;
  122. indexes[b] = k1;
  123. }
  124. private boolean less(int a, int b) {
  125. return keys[indexes[a]].compareTo(keys[indexes[b]]) < 0;
  126. }
  127. private void validateIndex(int k) {
  128. if (!contains(k)) {
  129. throw new NoSuchElementException("索引" + k + "不在优先队列中");
  130. }
  131. }
  132. }

注意对比最小堆和最小索引堆的 swap(int a, int b) 方法以及 less(int a, int b) 方法,在交换堆中的元素时使用的依据是元素的大小,交换之后无需调整 keys[],而是交换 nodeIndexes[]indexes[] 中的元素。

实现算法

通过上述的分析,实现 Dijkstra 算法就很简单了,时间复杂度为 \(O(|E|\log |V|)\):

  1. package com.zhiyiyo.graph;
  2. import com.zhiyiyo.collection.queue.IndexMinPriorQueue;
  3. import com.zhiyiyo.collection.stack.LinkStack;
  4. import com.zhiyiyo.collection.stack.Stack;
  5. import java.util.Arrays;
  6. public class DijkstraSP implements ShortestPath {
  7. private double[] distTo;
  8. private DirectedEdge[] edgeTo;
  9. private IndexMinPriorQueue<Double> pq;
  10. private int s;
  11. public DijkstraSP(WeightedDigraph graph, int s) {
  12. pq = new IndexMinPriorQueue<>(graph.V());
  13. edgeTo = new DirectedEdge[graph.V()];
  14. // 初始化距离
  15. distTo = new double[graph.V()];
  16. Arrays.fill(distTo, Double.POSITIVE_INFINITY);
  17. distTo[s] = 0;
  18. visit(graph, s);
  19. while (!pq.isEmpty()) {
  20. visit(graph, pq.pop());
  21. }
  22. }
  23. private void visit(WeightedDigraph graph, int v) {
  24. for (DirectedEdge edge : graph.adj(v)) {
  25. int w = edge.to();
  26. if (distTo[w] > distTo[v] + edge.getWeight()) {
  27. distTo[w] = distTo[v] + edge.getWeight();
  28. edgeTo[w] = edge;
  29. pq.set(w, distTo[w]);
  30. }
  31. }
  32. }
  33. // 省略已实现的方法 ...
  34. }

后记

Dijkstra 算法还能继续优化,将最小索引堆换成斐波那契堆之后时间复杂度为 \(O(|E|+|V|\log |V|)\),这里就不写了(因为还没学到斐波那契堆),以上~~

如何在 Java 中实现 Dijkstra 最短路算法的更多相关文章

  1. 如何在JAVA中实现一个固定最大size的hashMap

    如何在JAVA中实现一个固定最大size的hashMap 利用LinkedHashMap的removeEldestEntry方法,重载此方法使得这个map可以增长到最大size,之后每插入一条新的记录 ...

  2. 如何在java中使用sikuli进行自动化测试

    很早之前写过一篇介绍sikuli的文章.本文简单介绍如何在java中使用sikuli进自动化测试. 图形脚本语言sikuli sikuli IDE可以完成常见的单击.右击.移动到.拖动等鼠标操作,ja ...

  3. 如何在Java中调用Python代码

    有时候,我们会碰到这样的问题:与A同学合作写代码,A同学只会写Python,而不会Java, 而你只会写Java并不擅长Python,并且发现难以用Java来重写对方的代码,这时,就不得不想方设法“调 ...

  4. 如何在java中跳出当前多重嵌套循环?有几种方法?

    如何在java中跳出当前多重嵌套循环?有几种方法? - 两种方法   - 1.在外层循环定义标记          ok:          for(int i=0;i<100;i++){    ...

  5. 用代码说话:如何在Java中实现线程

    并发编程是Java语言的重要特性之一,"如何在Java中实现线程"是学习并发编程的入门知识,也是Java工程师面试必备的基础知识.本文从线程说起,然后用代码说明如何在Java中实现 ...

  6. 如何在Java中测试类是否是线程安全的

    通过优锐课的java核心笔记中,我们可以看到关于如何在java中测试类是否线程安全的一些知识点汇总,分享给大家学习参考. 线程安全性测试与典型的单线程测试不同.为了测试一个方法是否是线程安全的,我们需 ...

  7. 如何在 Java 中实现无向环和有向环的检测

    无向环 一个含有环的无向图如下所示,其中有两个环,分别是 0-2-1-0 和 2-3-4-2: 要检测无向图中的环,可以使用深度优先搜索.假设从顶点 0 出发,再走到相邻的顶点 2,接着走到顶点 2 ...

  8. 如何在 Java 中实现最小生成树算法

    定义 在一幅无向图 \(G=(V,E)\) 中,\((u, v)\) 为连接顶点 \(u\) 和顶点 \(v\) 的边,\(w(u,v)\) 为边的权重,若存在边的子集 \(T\subseteq E\ ...

  9. Dijkstra最短路算法

    Dijkstra最短路算法 --转自啊哈磊[坐在马桶上看算法]算法7:Dijkstra最短路算法 上节我们介绍了神奇的只有五行的Floyd最短路算法,它可以方便的求得任意两点的最短路径,这称为“多源最 ...

随机推荐

  1. MySQL基础_索引

    MySQL 索引(入门): 一.介绍 1.什么是索引? 一般的应用系统,读写比例在10:1左右,而且插入操作和一般的更新操作很少出现性能问题,在生产环境中,我们遇到最多的,也是最容易出问题的,还是一些 ...

  2. 在基于ABP框架的前端项目Vue&Element项目中采用电子签名的处理

    在前面随笔介绍了<在基于ABP框架的前端项目Vue&Element项目中采用电子签章处理文件和打印处理>的处理,有的时候,我们在流程中或者一些文件签署的时候,需要签上自己的大名,一 ...

  3. 《前端运维》一、Linux基础--09常用软件安装

    一.软件包管理 RPM是RedHat Package Manager(RedHat软件包管理工具)类似Windows里面的"添加/删除程序".软件包有几种类型,我们一起来看下: 源 ...

  4. Flask 之 蓝图

    蓝图,听起来就是一个很宏伟的东西 在Flask中的蓝图 blueprint 也是非常宏伟的 它的作用就是将 功能 与 主服务 分开怎么理解呢? 比如说,你有一个客户管理系统,最开始的时候,只有一个查看 ...

  5. Java的http post请求01之HttpURLConnection

    package com.ricoh.rapp.ezcx.iwbservice.webservice; import java.io.BufferedOutputStream; import java. ...

  6. S2-048(RCE远程代码执行)

    环境搭建: https://blog.csdn.net/qq_36374896/article/details/84145020 进入漏洞环境 cd vulhub-master/struts2/s2- ...

  7. 17调试经验之串口读写flash协议

    一是设计功能 我的理解协议就是一个命令包,通过给出不同的控制命令,来调动不同的功能模块,实现不同的功能,如读数据,写数据,擦除等. 二设计过程 先看了尤老师的视频,主要讲了大致设计原理和总体框架,当然 ...

  8. SpringBoot:自定义注解实现后台接收Json参数

    0.需求 在实际的开发过程中,服务间调用一般使用Json传参的模式,SpringBoot项目无法使用@RequestParam接收Json传参 只有@RequestBody支持Json,但是每次为了一 ...

  9. flexible如何实现自动判断dpr?

    判断机型, 找出样本机型去适配. 比如iphone以6为样本, 宽度375px, dpr是2

  10. 怎么清屏?怎么退出当前命令?怎么执行睡眠?怎么查看当前用户 id?查看指定帮助用什么命令?

    清屏:clear 退出当前命令:ctrl+c 彻底退出 执行睡眠 :ctrl+z 挂起当前进程 fg 恢复后台 查看当前用户 id:"id":查看显示目前登陆账户的 uid 和 g ...