前面说到“原生的Dijkstra”,由于Dijkstra采用的是贪心策略,在贪心寻找当前距离源结点最短的结点时需要遍历所有的结点,这必然会导致效率的下降,时间复杂度为n^n。因此当数据量较大时会消耗较长时间。为了提高Dijkstra的效率,只有对Dijkstra的贪心策略进行改进。
由于Dijkstra采用的贪心策略是每次寻找最短距离的结点并将其放入存放所有已知最短距离结点的S集合中,可以联想到堆以及优先级队列这些数据结构,这些结构都能非常高效地提供当前状态距离最短的结点。实践也可以证明这两种优化对于Dijkstra的效率提升是非常明显的。
另外需要提到的是优先队列优化的Dijkstra和堆优化的Dijkstra效率相差不多,但是优先队列优化的方式却非常好实现,因为java类库中有PriorityQueue对象,该对象就是优先队列。但是对于堆优化来说就比较复杂了,因为需要自己动手实现最小堆,这样既复杂又容易出错,因此如果是想对Dijksra进行优化的话,首推PriorityQueue+Dijkstra即优先队列优化的Dijkstra。
下面还是以蓝桥杯的那道”最短路“题目为例展示Heap+Dijkstra堆优化的Dijkstra
算法训练 最短路 时间限制:1.0s 内存限制:256.0MB
问题描述
给定一个n个顶点,m条边的有向图(其中某些边权可能为负,但保证没有负环)。请你计算从1号点到其他点的最短路(顶点从1到n编号)。
输入格式
第一行两个整数n, m。
接下来的m行,每行有三个整数u, v, l,表示u到v有一条长度为l的边。
输出格式 共n-1行,第i行表示1号点到i+1号点的最短路。 样例输入 3 3
1 2 -1
2 3 -1
3 1 2 样例输出 -1
-2 数据规模与约定
对于10%的数据,n = 2,m = 2。
对于30%的数据,n <= 5,m <= 10。
对于100%的数据,1 <= n <= 20000,1 <= m <= 200000,-10000 <= l <= 10000,保证从任意顶点都能到达其他所有顶点。
下面是堆优化的Dijkstra代码:
import java.util.HashMap;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
/**
* Heap + Dijkstra算法求单源最短路径
* 采用邻接表存储图数据
* 邻接表结构:
* 头结点--Node对象数组
* 邻接节点--Node对象中的HashMap
*
* @author DuXiangYu
*
*/
public class DijKstra_link_Heap {
static int nodeCount;
static int edgeCount;
// 邻接表表头数组
static Node[] firstArray;
// 最短路径数组
static int[][] dist;
static int[] ref;
static int max = 1000000;
/**
* 结点类
*
* @author DuXiangYu
*/
static class Node {
// 邻接顶点map
private HashMap<Integer, Integer> map = null;
public void addEdge(int end, int edge) {
if (this.map == null) {
this.map = new HashMap<Integer, Integer>();
}
this.map.put(end, edge);
}
}
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
nodeCount = sc.nextInt();
edgeCount = sc.nextInt();
firstArray = new Node[nodeCount + 1];
for (int i = 0; i < nodeCount + 1; i++) {
firstArray[i] = new Node();
}
for (int i = 0; i < edgeCount; i++) {
int begin = sc.nextInt();
int end = sc.nextInt();
int edge = sc.nextInt();
firstArray[begin].addEdge(end, edge);
}
sc.close();
long begin = System.currentTimeMillis();
djst();
long end = System.currentTimeMillis();
System.out.println(end - begin + "ms");
}
/**
* Heap + Dijkstra算法实现
*/
private static void djst() {
dist = new int[2][nodeCount + 1];
ref = new int[nodeCount + 1];
Node tempNode = firstArray[1];
for (int i = 2; i < nodeCount + 1; i++) {
HashMap<Integer, Integer> tempMap = tempNode.map;
dist[0][i] = tempMap.containsKey(i) ? tempMap.get(i) : max;
dist[1][i] = i;
ref[i] = i;
minUp(i);
}
int flag = nodeCount;
while (flag >= 2) {
int index = dist[1][2];
changeKey(2, flag);
maxDown(2, --flag);
// 用indx这个点去更新它的邻接点到开始点的距离
HashMap<Integer, Integer> m = firstArray[index].map;
if (m == null) {
continue;
}
Set<Integer> set = m.keySet();
Iterator<Integer> it = set.iterator();
while (it.hasNext()) {
int num = it.next();
if (m.get(num) + dist[0][flag + 1] < dist[0][ref[num]]) {
dist[0][ref[num]] = m.get(num) + dist[0][flag + 1];
minUp(ref[num]);
}
}
}
for (int i = 2; i < nodeCount + 1; i++) {
System.out.println(dist[0][ref[i]]);
}
}
/**
* 最大值下沉
*
* @param index
* @param end
*/
private static void maxDown(int index, int end) {
int temp = dist[0][index];
int left = index * 2 - 1;
while (left <= end) {
// 判断左右子节点大小
if (left + 1 <= end && dist[0][left + 1] < dist[0][left]) {
left++;
}
// 如果左右子节点都比temp大的话结束下沉操作
if (dist[0][left] > temp) {
break;
}
// 交换子节点和父节点
changeKey(index, left);
index = left;
left = index * 2 - 1;
}
}
/**
* 小值上升
*
* @param n
*/
private static void minUp(int n) {
int f = (n + 1) / 2;
while (f >= 2 && dist[0][f] > dist[0][n]) {
changeKey(f, n);
n = f;
f = (n + 1) / 2;
}
}
/**
* 交换两个值
*
* @param a
* @param b
*/
private static void changeKey(int a, int b) {
int n = dist[1][a];
int m = dist[1][b];
int temp = ref[n];
ref[n] = ref[m];
ref[m] = temp;
temp = dist[0][a];
dist[0][a] = dist[0][b];
dist[0][b] = temp;
temp = dist[1][a];
dist[1][a] = dist[1][b];
dist[1][b] = temp;
}
}
性能测试:
对于“原生的Dijkstra”中的那个较大测试数据:10000个结点和100000条边来说,该优化的Dijkstra执行实现平均为250ms,而无优化的Dijkstra对于该数据的平均执行实现为2200ms,性能提升了88%,可以看得出来对于性能的提升是非常明显的: