1. 介紹
Dijsktra算法是大牛Dijsktra於1956年提出,用來解決有向圖單源最短路徑問題;但是不能解決負權的有向圖,若要解決負權圖則需要用到Bellman-Ford算法。Dijsktra算法思想:在DFS遍歷圖的過程中,每一次取出離源點的最近距離的點,將該點標記為已訪問,松弛與該點相鄰的結點。
有向圖記為\(G=(n, m)\),其中,\(n\)為頂點數,\(m\)為邊數;且\(e[a,b]\)表示從結點\(a\)到結點\(b\)的邊。\(d[i]\)記錄源點到結點i的距離,\(U\)為未訪問的結點集合,\(V\)為已訪問的結點集合。Dijsktra算法具體步驟如下:
- 從集合\(U\)中尋找離源點最近的結點\(u\),並將結點\(u\)標記為已訪問(從集合\(U\)中移到集合\(V\)中)
- 松弛與結點\(u\)相鄰的未訪問結點,更新d數組
- 重復上述操作\(n\)次,即訪問了所有結點,集合\(U\)為空
Dijsktra算法的Java實現
/**
* Dijkstra's Algorithm for finding the shortest path
*
* @param adjMatrix adjacency matrix representation of the graph
* @param source the source vertex
* @param dest the destination vertex
* @return the cost for the shortest path
*/
public static int dijkstra(int[][] adjMatrix, int source, int dest) {
int numVertex = adjMatrix.length, minVertex = source;
// `d` marks the cost for the shortest path, `visit` marks whether has been visited or not
int[] d = new int[numVertex], visit = new int[numVertex];
Arrays.fill(d, Integer.MAX_VALUE);
d[source] = 0;
for (int cnt = 1; cnt <= numVertex; cnt++) {
int lowCost = Integer.MAX_VALUE;
// find the min-vertex which is the nearest among the unvisited vertices
for (int i = 0; i < numVertex; i++) {
if (visit[i] == 0 && d[i] < lowCost) {
lowCost = d[i];
minVertex = i;
}
}
visit[minVertex] = 1;
if (minVertex == dest) return d[dest];
// relax the minVertex's adjacency vertices
for (int i = 0; i < numVertex; i++) {
if (visit[i] == 0 && adjMatrix[minVertex][i] != Integer.MAX_VALUE) {
d[i] = Math.min(d[i], d[minVertex] + adjMatrix[minVertex][i]);
}
}
}
return d[dest];
}
復雜度分析
- 時間復雜度:重復操作(即最外層for循環)n次,找出minNode操作、松弛操作需遍歷所有結點,因此復雜度為\(O(n^2)\).
- 空間復雜度:開辟兩個長度為n的數組d與visit,因此復雜度為\(T(n)\).
2. 優化
從上述Java實現中,我們發現:(里層for循環)尋找距離源點最近的未訪問結點\(u\),通過遍歷整個數組來實現的,缺點是重復訪問已經訪問過的結點,浪費了時間。首先,我們來看看堆的性質。
堆
堆是一種完全二叉樹(complete binary tree);若其高度為h,則1~h-1層都是滿的。如果從左至右從上至下從1開始給結點編號,堆滿足:
- 結點\(i\)的父結點編號為\(i/2\).
- 結點\(i\)的左右孩子結點編號分別為\(2*i\), \(2*i+1\).
如果結點\(i\)的關鍵值小於父結點的關鍵值,則需要進行上浮操作(move up);如果結點\(i\)的關鍵值大於父結點的,則需要下沉操作(move down)。為了保持堆的整體有序性,通常下沉操作從根結點開始。
小頂堆優化Dijsktra算法
我們可以用小頂堆來代替d數組,堆頂對應於結點\(u\);取出堆頂,然后刪除,如此堆中結點都是未訪問的。同時為了記錄數組\(d[i]\)中索引\(i\)值,我們讓每個堆結點掛兩個值——頂點、源點到該頂點的距離。算法偽代碼如下:
Insert(vertex 0, 0) // 插入源點
FOR i from 1 to n-1: // 初始化堆
Insert(vertex i, infinity)
FOR k from 1 to n:
(i, d) := DeleteMin()
FOR all edges ij:
IF d + edge(i,j) < j’s key
DecreaseKey(vertex j, d + edge(i,j))
- Insert(vertex i, d)指在堆中插入堆結點(i, d)。
- DeleteMin()指取出堆頂並刪除,時間復雜度為\(O(\log n)\)。
- DecreaseKey(vertex j, d + edge(i,j))是松弛操作,更新結點(vertex j, d + edge(i,j)),需要進行上浮,時間復雜度為\(O(\log n)\)。
我們需要n次DeleteMin,m次DecreaseKey,優化版的算法時間復雜度為\(O((n+m)\log n)\).
代碼實現
鄰接表
每一個鄰接表的表項包含兩個部分:頭結點、表結點,整個鄰接表可以用一個頭結點數組表示。下面給出其Java實現
public class AdjList {
private int V = 0;
private HNode[] adjList =null; //鄰接表
/*表結點*/
class ArcNode {
int adjvex, weight;
ArcNode next;
public ArcNode(int adjvex, int weight) {
this.adjvex = adjvex;
this.weight = weight;
next = null;
}
}
/*頭結點*/
class HNode {
int vertex;
ArcNode firstArc;
public HNode(int vertex) {
this.vertex = vertex;
firstArc = null;
}
}
/*構造函數*/
public AdjList(int V) {
this.V = V;
adjList = new HNode[V+1];
for(int i = 1; i <= V; i++) {
adjList[i] = new HNode(i);
}
}
/*添加邊*/
public void addEdge(int start, int end, int weight) {
ArcNode arc = new ArcNode(end, weight);
ArcNode temp = adjList[start].firstArc;
adjList[start].firstArc = arc;
arc.next = temp;
}
public int getV() {
return V;
}
public HNode[] getAdjList() {
return adjList;
}
}
小頂堆
public class Heap {
public int size = 0 ;
public Node[] h = null; //堆結點
/*記錄Node中vertex對應堆的位置*/
public int[] index = null;
/*堆結點:
* 存儲結點+源點到該結點的距離
*/
public class Node {
int vertex, weight;
public Node(int vertex, int weight) {
this.vertex = vertex;
this.weight = weight;
}
}
public Heap(int maximum) {
h = new Node[maximum];
index = new int[maximum];
}
/*上浮*/
public void moveUp(int pos) {
Node temp = h[pos];
for (; pos > 1 && h[pos/2].weight > temp.weight; pos/=2) {
h[pos] = h[pos/2];
index[h[pos].vertex] = pos; //更新位置
}
h[pos] = temp;
index[h[pos].vertex] = pos;
}
/*下沉*/
public void moveDown( ) {
Node root = h[1];
int pos = 1, child = 1;
for(; pos <= size; pos = child) {
child = 2*pos;
if(child < size && h[child+1].weight < h[child].weight)
child++;
if(h[child].weight < root.weight) {
h[pos] = h[child];
index[h[pos].vertex] = pos;
} else {
break;
}
}
h[pos] = root;
index[h[pos].vertex] = pos;
}
/*插入操作*/
public void insert(int v, int w) {
h[++size] = new Node(v, w);
moveUp(size);
}
/*刪除堆頂元素*/
public Node deleteMin( ) {
Node result = h[1];
h[1] = h[size--];
moveDown();
return result;
}
}
優化算法
public class ShortestPath {
private static final int inf = 0xffffff;
public static void dijkstra(AdjList al) {
int V = al.getV();
Heap heap = new Heap(V+1);
heap.insert(1, 0);
for(int i = 2; i <= V; i++) {
heap.insert(i, inf);
}
for(int k =1; k <= V; k++) {
Heap.Node min = heap.deleteMin();
if(min.vertex == V) {
System.out.println(min.weight);
break;
}
AdjList.ArcNode arc = al.getAdjList()[min.vertex].firstArc;
while(arc != null) {
if((min.weight+ arc.weight) < heap.h[heap.index[arc.adjvex]].weight) {
heap.h[heap.index[arc.adjvex]].weight = min.weight+ arc.weight;
heap.moveUp(heap.index[arc.adjvex]);
}
arc = arc.next;
}
}
}
/*main方法用於測試*/
public static void main(String[] args) {
AdjList al = new AdjList(5);
al.addEdge(1, 2, 20);
al.addEdge(2, 3, 30);
al.addEdge(3, 4, 20);
al.addEdge(4, 5, 20);
al.addEdge(1, 5, 100);
dijkstra(al);
}
}
3. 參考資料#
[1] FRWMM, ALGORITHMS - DIJKSTRA WITH HEAPS.
