數(shù)據(jù)結(jié)構(gòu)與算法--最短路徑之Dijkstra算法
加權(quán)圖中,我們很可能關(guān)心這樣一個(gè)問題:從一個(gè)頂點(diǎn)到另一個(gè)頂點(diǎn)成本最小的路徑。比如從成都到北京,途中還有好多城市,如何規(guī)劃路線,能使總路程最小;或者我們看重的是路費(fèi),那么如何選擇經(jīng)過的城市可以使得總路費(fèi)降到最低?
- 首先路徑是有向的,最短路徑需要考慮到各條邊的方向。
- 權(quán)值不一定就是指距離,還可以是費(fèi)用等等...
最短路徑的定義:在一幅有向加權(quán)圖中,從頂點(diǎn)s到頂點(diǎn)t的最短路徑是所有從s到t的路徑中權(quán)值最小者。
為此,我們先要定義有向邊以及有向圖。
加權(quán)有向圖的實(shí)現(xiàn)
首先是有向邊。
package Chap7;
public class DiEdge {
private int from;
private int to;
private double weight;
public DiEdge(int from, int to, double weight) {
this.from = from;
this.to = to;
this.weight = weight;
}
public int from() {
return from;
}
public int to() {
return to;
}
public double weight() {
return weight;
}
@Override
public String toString() {
return "(" +
from +
"->" + to +
" " + weight +
')';
}
}
比起無向邊Edge類,更簡(jiǎn)單些,因?yàn)閮蓚€(gè)頂點(diǎn)有明顯的先后順序。
然后是加權(quán)有向圖。
package Chap7;
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
public class EdgeWeightedDiGraph<Item> {
private int vertexNum;
private int edgeNum;
// 鄰接表
private List<List<DiEdge>> adj;
// 頂點(diǎn)信息
private List<Item> vertexInfo;
public EdgeWeightedDiGraph(List<Item> vertexInfo) {
this.vertexInfo = vertexInfo;
this.vertexNum = vertexInfo.size();
adj = new ArrayList<>();
for (int i = 0; i < vertexNum; i++) {
adj.add(new LinkedList<>());
}
}
public EdgeWeightedDiGraph(List<Item> vertexInfo, int[][] edges, double[] weight) {
this(vertexInfo);
for (int i = 0; i < edges.length; i++) {
DiEdge edge = new DiEdge(edges[i][0], edges[i][1], weight[i]);
addDiEdge(edge);
}
}
public EdgeWeightedDiGraph(int vertexNum) {
this.vertexNum = vertexNum;
adj = new ArrayList<>();
for (int i = 0; i < vertexNum; i++) {
adj.add(new LinkedList<>());
}
}
public EdgeWeightedDiGraph(int vertexNum, int[][] edges, double[] weight) {
this(vertexNum);
for (int i = 0; i < edges.length; i++) {
DiEdge edge = new DiEdge(edges[i][0], edges[i][1], weight[i]);
addDiEdge(edge);
}
}
public void addDiEdge(DiEdge edge) {
adj.get(edge.from()).add(edge);
edgeNum++;
}
// 返回與某個(gè)頂點(diǎn)依附的所有邊
public Iterable<DiEdge> adj(int v) {
return adj.get(v);
}
public List<DiEdge> edges() {
List<DiEdge> edges = new LinkedList<>();
for (int i = 0; i < vertexNum; i++) {
for (DiEdge e : adj(i)) {
edges.add(e);
}
}
return edges;
}
public int vertexNum() {
return vertexNum;
}
public int edgeNum() {
return edgeNum;
}
public Item getVertexInfo(int i) {
return vertexInfo.get(i);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append(vertexNum).append("個(gè)頂點(diǎn), ").append(edgeNum).append("條邊。\n");
for (int i = 0; i < vertexNum; i++) {
sb.append(i).append(": ").append(adj.get(i)).append("\n");
}
return sb.toString();
}
public static void main(String[] args) {
List<String> vertexInfo = List.of("v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7");
int[][] edges = {{4, 5}, {5, 4}, {4, 7}, {5, 7}, {7, 5}, {5, 1}, {0, 4}, {0, 2},
{7, 3}, {1, 3}, {2, 7}, {6, 2}, {3, 6}, {6, 0}, {6, 4}};
double[] weight = {0.35, 0.35, 0.37, 0.28, 0.28, 0.32, 0.38, 0.26, 0.39, 0.29,
0.34, 0.40, 0.52, 0.58, 0.93};
EdgeWeightedDiGraph<String> graph = new EdgeWeightedDiGraph<>(vertexInfo, edges, weight);
System.out.println("該圖的鄰接表為\n"+graph);
System.out.println("該圖的所有邊:"+ graph.edges());
}
}
實(shí)現(xiàn)和加權(quán)無向圖差不多,就改了addEdge
和adj
方法。addEdge由于邊有向,不會(huì)對(duì)稱地存儲(chǔ)邊;adj方法不像無向圖那樣鄰接表中有重復(fù)的邊,有向圖中鄰接表中的邊都是唯一的,所以全部加入即可。
最短路徑的數(shù)據(jù)結(jié)構(gòu)
1、最短路徑樹中的邊
和深度優(yōu)先、廣度優(yōu)先搜索一樣,我們將用到一個(gè)edgeTo[]
表示一個(gè)樹形結(jié)構(gòu),edgeTo[v]
表示樹中連接頂點(diǎn)v和其父結(jié)點(diǎn)的邊(也就是起點(diǎn)s到v的路徑上最后一條邊)。
2、起點(diǎn)到各個(gè)頂點(diǎn)的最短距離
和Prim算法類似,需要一個(gè)distTo[]
。Prim算法中它存放的是:到某個(gè)頂點(diǎn)權(quán)值最小的那條邊。而最短路徑中,distTo[v]
存放的是:從起點(diǎn)s開始到某頂點(diǎn)v的最短路徑長(zhǎng)度。我們約定到起點(diǎn)s的最短路徑長(zhǎng)度為0,即distTo[s] = 0
;同時(shí)約定從起點(diǎn)s到不可達(dá)的頂點(diǎn)的距離均為正無窮。
最短路徑算法的基礎(chǔ)基于一個(gè)被稱為松弛的簡(jiǎn)單操作。放松一條邊v -> w意味著檢查s到w的最短路徑是否是 先從s到v,再?gòu)膙到w。如果是就更新相關(guān)數(shù)據(jù)結(jié)構(gòu)的內(nèi)容;如果不是,不作更改。用代碼可以表示為
// v -> w, v和w是邊edge的兩個(gè)頂點(diǎn)
// distTo[v] :s到v的最短距離;distTo[w]:s到w的最短距離
if (distTo[v] + edge.weight() < distTo[w]) {
distTo[w] = distTo[v] + e.weight();
edgeTo[w] = edge;
}
再用一幅圖加深理解。
先看左邊兩個(gè)圖:s到v的最短距離是3.1,s到w的最短距離是3.3。當(dāng)在頂點(diǎn)v時(shí),檢查它的鄰接點(diǎn)w,邊v -> w的權(quán)值是1.3,從s到w的當(dāng)然不能先從s到v,再?gòu)膙到w,因?yàn)檫@倆加起來都4.4,比原來s到w的方案還要費(fèi)勁,所以不會(huì)更改distTo[w]
和edgeTo[w]
。此時(shí)我們說v -> w這條邊失效并忽略它。
再看右邊兩個(gè)圖:原先s到w的方案距離為7.2,現(xiàn)在我們換條路走,從s先到v,再?gòu)膙到w,只有4.4!這是條到w更近的路。所以更新,distTo[w]
改成4.4,到s到w的最后一條邊edgeTo[w]
也改成了v- > w這條邊。此時(shí)就稱邊v -> w放松成功(可以想象成一根緊繃的橡皮筋,它的長(zhǎng)度比較長(zhǎng);橡皮筋放松后,長(zhǎng)度變短。)
對(duì)頂點(diǎn)的放松就是:放松由該頂點(diǎn)引出的所有邊。
在實(shí)現(xiàn)之前,對(duì)于最短路徑算法我們需要了解得更多,來看幾個(gè)命題。
- 當(dāng)且僅當(dāng)對(duì)于從v -> w的任意一條邊,都有
dist[w] <= distTo[v] + edge.weight()
,那么s到w的路徑都是最短路徑。 - Dijkstra算法能解決邊權(quán)值非負(fù)的加權(quán)有向圖的單點(diǎn)最短路徑問題,換句話說,當(dāng)遇到有負(fù)權(quán)值的邊,或者想通過一次運(yùn)算就找到任意頂點(diǎn)到任意頂點(diǎn)的最短路徑,Dijkstra就不適用了。
- 如果v是從起點(diǎn)s可達(dá)的,那么邊v -> w只會(huì)被放松一次,放松v時(shí),必有
dist[w] <= distTo[v] + edge.weight()
,該等式在算法整個(gè)流程都成立,所以distTo[w]
只能減小。而distTo[v]不會(huì)改變,因?yàn)槊看味歼x擇distTo[]最小的頂點(diǎn),之后的放松操作不可能使得任何distTo[]的值小于dist[v]。也就是說,每次選擇distTo[]最小的頂點(diǎn),它的值不會(huì)小于那些已經(jīng)放松過的頂點(diǎn)的最短路徑值distTo[v],也不會(huì)大于任意未被放松過的頂點(diǎn)。所有從s可達(dá)的頂點(diǎn)都會(huì)按照distTo[]里最短路徑的權(quán)值來依次放松。 - 最短路徑算法也可以處理無向圖,用有向圖的數(shù)據(jù)類型,只是對(duì)應(yīng)于無向圖,每條邊都會(huì)創(chuàng)建兩條方向不同的有向邊。例如,無向圖中的邊3-0,使用有向圖創(chuàng)建3 -> 0和0 -> 3兩條邊,然后調(diào)用最短路徑算法即可。
Dijkstra算法的實(shí)現(xiàn)
package Chap7;
import java.util.*;
public class Dijkstra {
private DiEdge[] edgeTo;
private double[] distTo;
private Map<Integer, Double> minDist;
public Dijkstra(EdgeWeightedDiGraph<?> graph, int s) {
edgeTo = new DiEdge[graph.vertexNum()];
distTo = new double[graph.vertexNum()];
minDist = new HashMap<>();
for (int i = 0; i < graph.vertexNum(); i++) {
distTo[i] = Double.POSITIVE_INFINITY; // 1.0 / 0.0為INFINITY
}
// 到起點(diǎn)距離為0
distTo[s] = 0.0;
relax(graph, s);
while (!minDist.isEmpty()) {
relax(graph, delMin());
}
}
private int delMin() {
Set<Map.Entry<Integer, Double>> entries = minDist.entrySet();
Map.Entry<Integer, Double> min = entries.stream().min(Comparator.comparing(Map.Entry::getValue)).get();
int key = min.getKey();
minDist.remove(key);
return key;
}
private void relax(EdgeWeightedDiGraph<?> graph, int v) {
for (DiEdge edge : graph.adj(v)) {
int w = edge.to();
if (distTo[v] + edge.weight() < distTo[w]) {
distTo[w] = distTo[v] + edge.weight();
edgeTo[w] = edge;
if (minDist.containsKey(w)) {
minDist.replace(w, distTo[w]);
System.out.println(w);
} else {
minDist.put(w, distTo[w]);
}
}
}
}
public double distTo(int v) {
return distTo[v];
}
public boolean hasPathTo(int v) {
return distTo[v] != Double.POSITIVE_INFINITY;
}
public Iterable<DiEdge> pathTo(int v) {
if (hasPathTo(v)) {
LinkedList<DiEdge> path = new LinkedList<>();
for (DiEdge edge = edgeTo[v]; edge != null; edge = edgeTo[edge.from()]) {
path.push(edge);
}
return path;
}
return null;
}
public static void main(String[] args) {
List<String> vertexInfo = List.of("v0", "v1", "v2", "v3", "v4", "v5", "v6", "v7");
int[][] edges = {{4, 5}, {5, 4}, {4, 7}, {5, 7}, {7, 5}, {5, 1}, {0, 4}, {0, 2},
{7, 3}, {1, 3}, {2, 7}, {6, 2}, {3, 6}, {6, 0}, {6, 4}};
double[] weight = {0.35, 0.35, 0.37, 0.28, 0.28, 0.32, 0.38, 0.26, 0.39, 0.29,
0.34, 0.40, 0.52, 0.58, 0.93};
EdgeWeightedDiGraph<String> graph = new EdgeWeightedDiGraph<String>(vertexInfo, edges, weight);
Dijkstra dijkstra = new Dijkstra(graph, 0);
for (int i = 0; i < graph.vertexNum(); i++) {
System.out.print("0 to " + i + ": ");
System.out.print("(" + dijkstra.distTo(i) + ") ");
System.out.println(dijkstra.pathTo(i));
}
}
}
/* Outputs
0 to 0: (0.0) []
0 to 1: (1.05) [(0->4 0.38), (4->5 0.35), (5->1 0.32)]
0 to 2: (0.26) [(0->2 0.26)]
0 to 3: (0.9900000000000001) [(0->2 0.26), (2->7 0.34), (7->3 0.39)]
0 to 4: (0.38) [(0->4 0.38)]
0 to 5: (0.73) [(0->4 0.38), (4->5 0.35)]
0 to 6: (1.5100000000000002) [(0->2 0.26), (2->7 0.34), (7->3 0.39), (3->6 0.52)]
0 to 7: (0.6000000000000001) [(0->2 0.26), (2->7 0.34)]
*/
和Prim算法的即時(shí)版本的幾乎一樣!兩種算法都是添加邊的方式來構(gòu)造一棵樹:Prim算法每次添加的是離整棵樹(各個(gè)頂點(diǎn))最近的樹外的頂點(diǎn);Dijkstra算法每次添加的是離起點(diǎn)最近的樹外頂點(diǎn)。
Dijkstra不需要marked[]
來記錄被訪問過的頂點(diǎn)了,因?yàn)槊織l邊v -> w只會(huì)被放松一次,每個(gè)頂點(diǎn)也只會(huì)放松一次。放松后的頂點(diǎn)的最短路徑長(zhǎng)度一定滿足dist[w] <= distTo[v] + edge.weight()
,當(dāng)想重復(fù)放松某個(gè)頂點(diǎn)時(shí),會(huì)因?yàn)闊o法通過以下條件而被跳過。
if (distTo[v] + edge.weight() < distTo[w]) { }
我們還是來跟著圖走一遍。
- 放松頂點(diǎn)0,2、4被加入Map,distTo[2]為0 -> 2的權(quán)值,distTo[4]為0 -> 4的權(quán)值。
- 按權(quán)值放松頂點(diǎn)2,0 -> 2添加到樹中。7被加入Map。distTo[7]為0 -> 2 -> 7的權(quán)值和。
- 放松頂點(diǎn)4,0 -> 4被加入到樹中。5加到Map。dsitTo[5]為0 -> 4 -> 5的權(quán)值和。0 -> 4 -> 7沒有0 ->2 -> 7路徑短所以不更新distTo[7]。
- 放松頂點(diǎn)7,2- > 7加入到樹中。3加入到Map。distTo[3]為0 -> 2 -> 3 -> 7的權(quán)值和,0 -> 2 -> 7 -> 5的權(quán)值和沒有0 -> 4 -> 5的權(quán)值和小,所有不更新distTo[5]
- 放松頂點(diǎn)5, 4 ->5加入到樹中,1加入到Map,distTo[1]為0 -> 4 -> 5 -> 1的權(quán)值和。0 -> 4 -> 5 -> 7的權(quán)值和沒有0 -> 2 -> 7的權(quán)值和小,所以不更新distTo[7]
- 放松頂點(diǎn)3,7 -> 3加入到樹中。6加入到Map。distTo[6]為0 -> 2 -> 7 -> 3 -> 6的權(quán)值和。
- 放松頂點(diǎn)1,5 -> 1加入到樹。0 -> 4 -> 5 ->1 -> 3的權(quán)值和由于沒有0 -> 2 -> 7 -> 3 的權(quán)值和小,所以不更新distTo[3]。
- 放松頂點(diǎn)6, 3 -> 6加入到樹中。至此所有頂點(diǎn)都已放松一次,算法結(jié)束。
by @sunhaiyu
2017.9.23