最短路径算法之——Dijkstra算法介绍与实现

我们使用导航应用时,选择好出发地和目的地,就能计算出两地的最短距离,而且能显示出到各个路口的距离。这个场景映射到数据结构和算法中,其实是求图中两个顶点间的最短距离。由于是从一个顶点到另外一个顶点,涉及到方向以及不同顶点间的距离,因此是带权重的有向图。

我们先大概分析下这个问题。

一般我们的想法是两顶点间直达的距离是最短的,但存在通过其他顶点“绕路”后反而距离更短的情况,例如求b到c点的最短路径时,经过a点再到c,反而优于b-c直达距离。

但即使这次绕路后是最短路径,再次绕路反而会更远。如下图求b到d最短路径,到达c点后选择直达的c-d反而更近。

因此我们不能只根据当前的路径值来进行选择,需要记录从开始顶点到每个后续顶点的累计距离,如果后续有其他边再次经过该顶点时,则判断新的累计距离是否小于先前的累计距离,小于则更新为这个更小的累计距离,表示发现了更短的路径。

例如求b-d最短路径。从b点出发时,我们发现共有a c两条路可以选择,那么就把a标记为距离1,c标记为距离4。接下来再分别从a、c两点对应的路径继续计算。a点对应的是c,这时发现c点之前已经存在距离4,于是把a点的累计路径1加上a-c路径长度2,再和c先前的累计距离4对比,1+2=3<4,说明b-a-c这个路径距离是短于b-c路径的,于是把c点累计距离更新为3,这样就解决了之前的绕路问题,保证c顶点的累计距离必然是之前最短的路径之和。

同时,我们可以看出b-a-c-d最短路径中,c点如果是最短路径中必然经过的一个点,则b到c的最短路径b-a-c也必然包含在最终的最短路径中,这其实就是Dijkstra算法的基础。

                   引用自Edsger W. Dijkstra "A Note on Two Problems in Connexion with Graphs"

 

按照这个思路我们来编写代码。演示数据如图,求顶点0到顶点6的最短路径。

代码

  1 import java.util.ArrayList;
  2 import java.util.Arrays;
  3 
  4 public class ShortestPath {
  5     private static final int MAX_WEIGHT = 999;
  6     private static final int NOT_EXISTS_PATH = -1;
  7 
  8     public static void main(String[] args) {
  9         //顶点个数
 10         int vertexNum = 7;
 11         //初始化演示数据
 12         ArrayList<Vertex>[] list = initDemo(vertexNum);
 13         print(list);
 14 
 15         //开始顶点
 16         int startVertex = 0;
 17         //结束顶点
 18         int endVertex = 6;
 19 
 20         //记录访问过的顶点,默认false。记录每次可选路径中对应路径最小的顶点
 21         boolean[] visit = new boolean[vertexNum];
 22 
 23         //记录每个顶点到前一个顶点的路径之和,默认为最大值。如果新的路径小于这个数,说明需要更新
 24         int[] dist = new int[vertexNum];
 25         Arrays.fill(dist, MAX_WEIGHT);
 26 
 27         //记录顶点对应的前一个顶点,用于还原最小路径
 28         int[] prev = new int[vertexNum];
 29         Arrays.fill(prev, NOT_EXISTS_PATH);
 30 
 31         //需要把开始顶点的路径和设置为0,这样才能保证首次选择的必然是开始顶点(其他顶点的路径和都是最大值)
 32         dist[startVertex] = 0;
 33         while (true) {
 34             //取未访问过的顶点中,最小路径和的顶点
 35             int minVertex = findMin(dist, visit);
 36             if (minVertex == NOT_EXISTS_PATH || minVertex == endVertex) {
 37                 break;
 38             }
 39             visit[minVertex] = true;
 40 
 41             for (Vertex v : list[minVertex]) {
 42                 //当前路径值加前一个顶点路径值,如果小于当前顶点的累计值,需要更新当前顶点的累计值
 43                 if (!visit[v.id] && v.weight + dist[minVertex] < dist[v.id]) {
 44                     dist[v.id] = v.weight + dist[minVertex];
 45                     //更新路径为前一个顶点
 46                     prev[v.id] = minVertex;
 47                 }
 48             }
 49             System.out.printf("minVertex %s dist %s visit %s prev %s\n", minVertex, Arrays.toString(dist), Arrays.toString(visit), Arrays.toString(prev));
 50         }
 51 
 52         //打印最终路径
 53         int prevVertex = endVertex;
 54         while (prev[prevVertex] != NOT_EXISTS_PATH) {
 55             System.out.println(prevVertex + "-" + prev[prevVertex]);
 56             prevVertex = prev[prevVertex];
 57         }
 58     }
 59 
 60     //未访问过的顶点中的最小路径
 61     private static int findMin(int[] dist, boolean[] visit) {
 62         int minWeight = MAX_WEIGHT;
 63         int minVertex = NOT_EXISTS_PATH;
 64         for (int i = 0; i < dist.length; i++) {
 65             if (!visit[i] && dist[i] < minWeight) {
 66                 minWeight = dist[i];
 67                 minVertex = i;
 68             }
 69         }
 70         return minVertex;
 71     }
 72 
 73     private static ArrayList<Vertex>[] initDemo(int vertexNum) {
 74         ArrayList<Vertex>[] list = new ArrayList[vertexNum];
 75         String[] demo = new String[vertexNum];
 76         //顶点id, 顶点权重weight(距离);
 77         demo[0] = "1,4;2,6;3,6";
 78         demo[1] = "4,9;2,1";
 79         demo[2] = "4,6;5,4";
 80         demo[3] = "2,2;5,1";
 81         demo[4] = "6,6";
 82         demo[5] = "4,1;6,8";
 83         demo[6] = "";
 84 
 85         for (int i = 0; i < vertexNum; i++) {
 86             ArrayList<Vertex> vertexList = new ArrayList<>();
 87             if (demo[i] == null || demo[i].length() == 0) {
 88                 list[i] = vertexList;
 89                 continue;
 90             }
 91             String[] its = demo[i].split(";");
 92             for (String kv : its) {
 93                 String[] kvIts = kv.split(",");
 94                 vertexList.add(new Vertex(Integer.parseInt(kvIts[0]), Integer.parseInt(kvIts[1])));
 95             }
 96             list[i] = vertexList;
 97         }
 98         return list;
 99     }
100 
101     private static void print(ArrayList<Vertex>[] list) {
102         //打印顶点
103         int index = 0;
104         for (ArrayList<Vertex> arrayList : list) {
105             System.out.println(index + " " + arrayList);
106             index++;
107         }
108     }
109 
110     private static class Vertex {
111         int id;
112         int weight;
113 
114         public Vertex(int id, int weight) {
115             this.id = id;
116             this.weight = weight;
117         }
118 
119         @Override
120         public String toString() {
121             return "Vertex{" +
122                     "id=" + id +
123                     ", weight=" + weight +
124                     '}';
125         }
126     }
127 }

输出

0 [Vertex{id=1, weight=4}, Vertex{id=2, weight=6}, Vertex{id=3, weight=6}]  #演示数据对应的各顶点信息,id为顶点名称0-6,weight为路径长度
1 [Vertex{id=4, weight=9}, Vertex{id=2, weight=1}]
2 [Vertex{id=4, weight=6}, Vertex{id=5, weight=4}]
3 [Vertex{id=2, weight=2}, Vertex{id=5, weight=1}]
4 [Vertex{id=6, weight=6}]
5 [Vertex{id=4, weight=1}, Vertex{id=6, weight=8}]
6 []
minVertex 0 dist [0, 4, 6, 6, 999, 999, 999] visit [true, false, false, false, false, false, false] prev [-1, 0, 0, 0, -1, -1, -1] #minVertex是当前的最小路径的顶点0,dist为当前各顶点的路径和,visit为标记顶点是否访问 prev记录顶点的前一个顶点,用于记录最终的最短路径
minVertex 1 dist [0, 4, 5, 6, 13, 999, 999] visit [true, true, false, false, false, false, false] prev [-1, 0, 1, 0, 1, -1, -1]
minVertex 2 dist [0, 4, 5, 6, 11, 9, 999] visit [true, true, true, false, false, false, false] prev [-1, 0, 1, 0, 2, 2, -1]
minVertex 3 dist [0, 4, 5, 6, 11, 7, 999] visit [true, true, true, true, false, false, false] prev [-1, 0, 1, 0, 2, 3, -1]
minVertex 5 dist [0, 4, 5, 6, 8, 7, 15] visit [true, true, true, true, false, true, false] prev [-1, 0, 1, 0, 5, 3, 5]
minVertex 4 dist [0, 4, 5, 6, 8, 7, 14] visit [true, true, true, true, true, true, false] prev [-1, 0, 1, 0, 5, 3, 4]
6-4  #最终最短路径对应的各个边
4-5
5-3
3-0

流程图

可以看出算法核心是这三个数组:

一是顶点的状态数组visit,顶点一旦标记为访问过(true),后续就无需再处理这些顶点;

二是各顶点当前路径之和的dist数组,每次需要从中选出一条未访问过的顶点的最小路径和的顶点,再根据它取出对应的各边来处理;

三是存储各顶点前一个顶点的数组prev,用于还原出最短路径对应的各个边。

另外顶点数据存储用的是邻接表,数组下标为顶点id,值是一个ArrayList,里面存储了对应的顶点和权值。

 

参考资料
https://handwiki.org/wiki/:Dijkstra's%20algorithm
https://www.programiz.com/dsa/dijkstra-algorithm

posted @ 2022-07-27 13:30  binary220615  阅读(284)  评论(0编辑  收藏  举报