迪杰斯特拉算法(Dijkstra算法)
以下描述摘自《数据结构-C语言描述》耿国华 主编 第7章 235页
下面介绍由迪杰斯特拉(Dijkstra)提出的一个算法,求解从顶点 V0 出发到其余顶点的最短路径。该算法按照最短路径长度递增的顺序产生所有最短路径。
对于图 G = (V, E),将图中的顶点分成两组:
第一组 S : 已求出的最短路径的终点集合(开始为 V0)
第二组 V - S :尚未求出最短路径的顶点集合(开始为 V - { V0 } 的全部结点)
算法将按照最短路径长度的递增顺序逐个将第二组的顶点加入到第一组中,直到所有顶点都被加入到第一组顶点集 S 为止。
定理:下一条最短路径或者是弧 ( V0, Vx),或者是中间经过 S 中的某些顶点,而后到达 Vx 的路径。
证明:可用反证法:假设下一条最短路径上有一个顶点 Vy 不在 S 中,即此路径为 (V0,...,Vy,...,Vx)。显然 (V0,...,Vy)的长度小于(V0,...,Vy,...,Vx)的长度,
故下一条最短路径应为(V0,...,Vy),这与假设的下一条最短路径(V0,...,Vy,...,Vx)相矛盾!因此,下一条最短路径上不可能有不在 S 中的顶点 Vy,即假设不成立。
算法中使用了辅助数组 dist[ ],dist[ i ]表示目前已经找到的、从开始点 V0 到终点 Vi 的当前最短路径的长度。它的初值为:如果从 V0 到 Vi 有弧,则 dist[ i ]为弧
的权值;否则 dist[ i ]为 。
根据上述定理,长度最短的一条最短路径必为 (V0, Vk),Vk满足如下条件:
dist[ k ] = Min { dist[ i ] } vi V - S
求得顶点 Vk 的最短路径后,将 Vk 加入到第一组顶点集 S 中。
每加入一个新顶点 Vk 到顶点集 S ,则对第二组剩余的各个顶点而言,多了一个“中转”节点,从而多了一个“中转”路径,所以要对第二组剩余的各个顶点的最短
路径长度 dist[ i ]进行修正。
原来 V0 到 Vi 的最短路径长度为 dist[ i ],加进 Vk 之后,以 Vk 作为中间顶点的“中转”路径长度为:dist[ k ] + Wki,(Wki为弧<Vk, Vi>上的权值),若“中转”
路径长度小于 dist[ i ],则将顶点 Vi 的最短路径长度修正为“中转”路径长度。
修正后,再选则数组 dist[ ]中值最小的顶点加入到第一组顶点集 S 中,如此进行下去,直到图中所有顶点都加入到第一组顶点集 S 中为止。
另外,为了记录从 V0 出发到其余各点的最短路径(顶点序列),引进辅助数组 path[ ],path[ i ]表示目前已经找到的,从开始点 V0 到终点 Vi 的当前最短路径顶点序列。
它的初值为:如果从 V0 到 Vi 有弧,则path[ i ]为(V0,Vi);否则path[ i ]为空。
Dijkstra算法简介
算法特点
迪杰斯特拉算法使用了广度优先搜索解决赋权有向图或者无向图的单源最短路径问题,算法最终得到一个最短路径树。该算法常用于路由算法或者作为其他图算法的一个子模块。
算法思路
1、Dijkstra算法采用的是一种贪心的策略,声明一个数组dis来保存源点到各顶点的最短距离;T = {} 保存已经找到了最短路径的顶点集合;初始时,源点 s 的路径权重被赋为 0
(dis[s] = 0)。
2、若对于顶点 s 存在能直接到达的边 (s, m) ,则把 dis[m] 设为 w (s, m) ,同时把其他所有 s 不能直接到达的顶点的路径长度设为无穷大。初始时,集合 T 只有顶点 s 。
3、然后,从 dis 数组选择最小值,则该值就是源点 s 到该值对应的顶点的最短路径,并且把该点加入到 T 中,此时完成一个顶点的选取。
4、然后,我们需要看看新加入的顶点是否可以到达其他顶点(新加入顶点的邻接点集合)并且看看通过该顶点到达其他点的路径长度是否比源点直接到达短,如果是,那么就替换这些顶点在 dis 中的值。
5、然后,又从 dis 中找出最小值,重复上述动作,直到 T 中包含了图的所有顶点。
Dijkstra算法示例演示
以下图为例,找出从顶点v1到其他各顶点的最短路径。
执行步骤1、2:首先第一步,我们先声明一个dis数组,该数组初始化(源顶点v1到其他点的直接距离,无法直接到达则记为无穷)的值为:
我们的顶点集合 T 的初始化为:T = {v1}
执行步骤:3:在数组dis中顶点集合 V - T 中选取距离源点 v1最小值对应的顶点加入 T 集合, 此时对应 v3 v1 -> v3
T = {v1, v3}
执行步骤4:我们新加入的顶点为 v3, 从图中查看 v3 的所有邻接顶点集合 {v4}; 那么经过 v3 到达 v4的最短路径为 10 + 50 = 60,再看下 v1到v4 直达为无穷大,所以将 dis[3] 值设置为 60
执行步骤5:在数组dis中顶点集合 V - T 中选取距离源点 v1最小值对应的顶点加入 T 集合, 此时对应 v5 v1 -> v5
T = {v1, v3, v5}
我们新加入的顶点为 v5, 从图中查看 v5 的所有邻接顶点集合 {v4, v6} ; 那么经过 v5 到达 v4的最短路径为 30 + 20 = 50, 那么经过 v5 到达 v6的最短路径为 30 + 60 =90; 再观察 v1 -> v4 & v1 -> v6 取最小值 分别赋给 dis[3] & dis[5]
执行步骤5:在数组dis中顶点集合 V - T 中选取距离源点 v1最小值对应的顶点加入 T 集合, 此时对应 v4 v1 -> v5 -> v4
T = {v1, v3, v5, v4}
我们新加入的顶点为 v4, 从图中查看 v4 的所有邻接顶点集合 {v6} ; 那么经过 v4 到达 v6的最短路径为 50 + 10 = 60; 再看下 v1到v6 直达为100; 取最小值作为dis[5] 的值;
执行步骤5:在数组dis中顶点集合 V - T 中选取距离源点 v1最小值对应的顶点加入 T 集合, 此时对应 v6 v1 -> v5 -> v4 - > v6
T = {v1, v3, v5, v4, v6}
我们新加入的顶点为 v6, 从图中查看 v6 的所有邻接顶点集合 {} ; 则不做任何操作;
执行步骤5:在数组dis中顶点集合 V - T 中选取距离源点 v1最小值对应的顶点加入 T 集合, 此时对应 v2 无路径
T = {v1, v3, v5, v4, v6, v2}
我们新加入的顶点为 v2, 从图中查看 v2 的所有邻接顶点集合 {v3} ; 那么经过 v2 到达 v3的最短路径为 无穷大 + 5 = 无穷大;再看下 v1到v2 直达为无穷大; 取最小值作为dis[1]的值;
至此 V - T 已经空,运算结束;
最后,顶点集合: T = {v1, v3, v5, v4, v6, v2}
所以我们得到的最后的结果为:
源点 | 终点 | 最短路径 | 路径长度 |
v0 | v2 | v0,v2 | 10 |
v3 | v0,v2,v3 | 25 | |
v1 | v0,v2,v3,v1 | 45 | |
v4 | v0,v4 | 45 | |
v5 | 无最短路径 |
输入:
6 11
0 1 50
0 2 10
0 4 45
1 4 10
1 2 15
2 3 15
2 0 20
3 1 20
3 4 35
4 3 30
5 3 3
0 1
import java.nio.charset.StandardCharsets;
import java.util.*;
public class Dijkstra {
public static void main(String[] args) {
Scanner cin = new Scanner(System.in, StandardCharsets.UTF_8);
int routeNum = cin.nextInt();
int linkNum = cin.nextInt();
int[][] linkWeight = new int[linkNum][3];
for (int i = 0; i < linkNum; i++) {
for (int j = 0; j < 3; j++) {
linkWeight[i][j] = cin.nextInt();
}
}
int srcNode = cin.nextInt();
cin.close();
System.out.println(" ");
int[] dis = new int[routeNum];
Map<String, LinkedHashSet<Integer>> shortPathMap = shortPath(linkWeight, srcNode, dis);
for (String key : shortPathMap.keySet()) {
System.out.println(key + ": "+shortPathMap.get(key) + " " + dis[Integer.parseInt(key.split(",")[1])]);
}
}
public static Map<String, LinkedHashSet<Integer>> shortPath(int[][] linkWeight, int srcNode, int[] dis) {
Map<Integer, NodeInfo> nodeMap = new LinkedHashMap<>();
for (int[] linkInfo : linkWeight) {
NodeInfo preNode = nodeMap.computeIfAbsent(linkInfo[0], key -> new NodeInfo(linkInfo[0]));
NodeInfo nextNode = nodeMap.computeIfAbsent(linkInfo[1], key -> new NodeInfo(linkInfo[1]));
preNode.adjNodes.put(nextNode.value, linkInfo[2]);
}
// 源点 第一组 S集合 第二组 V - S 集合
Map<String, LinkedHashSet<Integer>> pathMap = new LinkedHashMap<>();
Map<Integer, NodeInfo> sourceMap = new LinkedHashMap<>();
// 目标节点, 源点到目标的路径权重大小
List<int[]> unknownSet = new LinkedList<>();
sourceMap.put(srcNode, nodeMap.get(srcNode));
String srcToDstKey = srcNode + "," + srcNode;
LinkedHashSet<Integer> path = pathMap.getOrDefault(srcToDstKey, new LinkedHashSet<>());
path.add(srcNode);
pathMap.put(srcToDstKey, path);
for (int i = 0; i < dis.length; i++) {
if (i == srcNode) {
dis[i] = 0;
continue;
}
dis[i] = nodeMap.get(srcNode).adjNodes.getOrDefault(i, Integer.MAX_VALUE);
unknownSet.add(new int[]{i, dis[i]});
}
while (unknownSet.size() > 0) {
// 在第二组集合中挑选距离源点最短(可能只剩下无限大的节点)的顶点加入第一个集合
unknownSet.sort(Comparator.comparingInt(o -> o[1]));
NodeInfo minPathNode = nodeMap.get(unknownSet.get(0)[0]);
// 新增节点 minPathNode.value
sourceMap.put(minPathNode.value, minPathNode);
srcToDstKey = srcNode + "," + minPathNode.value;
// 记录节点间最短路径
path = pathMap.getOrDefault(srcToDstKey, new LinkedHashSet<>());
if (dis[minPathNode.value] != Integer.MAX_VALUE) {
if (path.isEmpty()) {
path.add(srcNode);
}
path.add(minPathNode.value);
}
pathMap.put(srcToDstKey, path);
unknownSet.remove(0);
// 更新源点到新增顶点的邻接顶点集合每个顶点的最短距离
updateNewAddNodeAdjoinNodesPath(srcNode, pathMap, unknownSet, dis, minPathNode);
}
return pathMap;
}
private static void updateNewAddNodeAdjoinNodesPath(int srcNode, Map<String, LinkedHashSet<Integer>> pathMap, List<int[]> unknownSet, int[] dis, NodeInfo minPathNode) {
String srcToDstKey;
LinkedHashSet<Integer> path;
for (int nextNode : minPathNode.adjNodes.keySet()) {
if (dis[minPathNode.value] == Integer.MAX_VALUE) {
continue;
}
int routeSize = dis[minPathNode.value] + minPathNode.adjNodes.get(nextNode);
if (routeSize < dis[nextNode]) {
srcToDstKey = srcNode + "," + minPathNode.value;
// 获取之前路径的copy 在copy数据基础上增加内容赋值给更新后的节点,避免干扰中转节点的最短路径值
path = new LinkedHashSet<>(pathMap.getOrDefault(srcToDstKey, new LinkedHashSet<>()));
if (!path.isEmpty()) {
path.add(nextNode);
// 更新最短路径节点集合
pathMap.put(srcNode + "," + nextNode, path);
}
dis[nextNode] = routeSize;
}
updateUnknownSet(unknownSet, nextNode, dis[nextNode]);
}
}
private static void updateUnknownSet(List<int[]> unknownSet, int nodeValue, int newVal) {
for (int[] item : unknownSet) {
if (item[1] == nodeValue) {
item[0] = newVal;
return;
}
}
}
static class NodeInfo {
int value;
// 邻接点,权重
Map<Integer, Integer> adjNodes = new LinkedHashMap<>();
public NodeInfo(int val) {
this.value = val;
}
}