演算法(四):圖解狄克斯特拉演算法

CodeInfo發表於2019-03-04

演算法簡介

狄克斯特拉演算法(Dijkstra )用於計算出不存在非負權重的情況下,起點到各個節點的最短距離

可用於解決2類問題:

  • 從A出發是否存在到達B的路徑;
  • 從A出發到達B的最短路徑(時間最少、或者路徑最少等),事實上最後計算完成後,已經得到了A到各個節點的最短路徑了;

其思路為:

(1) 找出“最便宜”的節點,即可在最短時間內到達的節點。

(2) 更新該節點對應的鄰居節點的開銷,其含義將稍後介紹。

(3) 重複這個過程,直到對圖中的每個節點都這樣做了。

(4) 計算最終路徑。

案例

案例一

先舉個簡單的例子介紹其實現的思路:

如下圖按Dijkstra演算法思路獲取起點到終點的最短路徑

演算法(四):圖解狄克斯特拉演算法

1.首先列出起點到各個節點耗費的時間:

父節點 節點 耗時
起點 A 6分鐘
起點 B 2分鐘
.. 終點 ∞當做無窮大

2.獲取最便宜的節點,由上表知道,起點->B花費最少,計算節點B前往各個鄰居節點所需要的時間,並更新原本需要花費更多的時間的節點:

父節點 節點 耗時
B A 5分鐘 更新耗時,原本需要6分鐘
起點 B 2分鐘
B 終點 7分鐘 更新耗時

3.此時B新增進已處理的列表,不再處理了,現在只剩起點->A花費最少,計算節點A前往各個鄰居節點所需要的時間,並更新原本需要花費更多的時間的節點:

父節點 節點 耗時
- A 5分鐘 更新耗時,原本需要6分鐘
起點 B 2分鐘
A 終點 6分鐘 更新耗時,原本需要7分鐘

4.此時通過倒序可以知道: 起點 -> A -> 終點 , 該路徑即為最短的路徑,耗費6分鐘

案例二

這邊以之前的廣度優先搜尋演算法的例子進行舉例:

演算法(四):圖解狄克斯特拉演算法

如上圖所示,前邊使用廣度優先搜尋演算法可以得知A到H所需要的最少步驟的路徑為: A->B->E->H, 本次在各個路徑上新增所需花費的時間(當然代表公里數之類的也行),作為各個路段的權重。

現在A->B->E->H明顯就不再是花費時間最短的路線了,這種新增了權重求最短路徑的問題,可以使用狄克斯特拉演算法來解決:

1.獲取A到各個節點的耗時,A標誌為已處理:

父節點 節點 耗時
A B 5分鐘
A C 1分鐘
.. H ∞當做無窮大

2.找出A能到達的最短的節點,計算更新相鄰節點的耗時,此時最短的節點為C,C標誌為已處理

父節點 節點 耗時 標誌
A B 5分鐘
A C 1分鐘 C已處理
C D 6分鐘
C F 7分鐘
.. H ∞當做無窮大

2.找出A能到達的最短的節點,計算更新相鄰節點的耗時,此時最短的節點為B,B標誌為已處理

父節點 節點 耗時 標誌
A B 5分鐘 B已處理
A C 1分鐘 C已處理
C D 6分鐘
C F 7分鐘
B E 15分鐘
.. H ∞當做無窮大

3.A相鄰節點已處理完,此時找出C能到達的最短的節點,此時為C->D,計算更新相鄰節點的耗時,D標誌為已處理

父節點 節點 耗時 標誌
A B 5分鐘 B已處理
A C 1分鐘 C已處理
C D 6分鐘 D已處理
C F 7分鐘
D E 9分鐘
.. H ∞當做無窮大

4.同理更新 F

父節點 節點 耗時 標誌
A B 5分鐘 B已處理
A C 1分鐘 C已處理
C D 6分鐘 D已處理
C F 7分鐘 F已處理
D E 9分鐘
F G 9分鐘
.. H ∞當做無窮大

4.同理更新 E

父節點 節點 耗時 標誌
A B 5分鐘 B已處理
A C 1分鐘 C已處理
C D 6分鐘 D已處理
C F 7分鐘 F已處理
D E 9分鐘 E已處理
F G 9分鐘
E H 12分鐘

5.同理更新 G

父節點 節點 耗時 標誌
A B 5分鐘 B已處理
A C 1分鐘 C已處理
C D 6分鐘 D已處理
C F 7分鐘 F已處理
D E 9分鐘 E已處理
F G 9分鐘 G已處理,G->H並沒有降低花費,所以不更新H耗時
E H 12分鐘

經過如上步驟後邊可以通過倒序得到最短路徑為:

A->H的最短路徑為:A->C->D->E->H ,總耗時12分鐘; 表中其餘結果也是最短路徑了,比如A->G最短路徑: A->C->F->G

侷限性

該演算法不適合負權重的情況,案例三:

演算法(四):圖解狄克斯特拉演算法

這邊使用該演算法看看為何不適合,計算A->E:

1.首先起點A出發

父節點 節點 耗時
A B 100元
A C 1元
.. 終點 ∞當做無窮大

2.計算最便宜節點C

父節點 節點 耗時 標誌
A B 100元
A C 1元 C已處理
C D 2元
.. 終點 ∞當做無窮大

2.計算最便宜節點B

父節點 節點 耗時 標誌
A B 100元 B已處理
B C -100元 C已處理,但是這邊依舊需要更新了C
C D 2元
.. 終點 ∞當做無窮大

3.計算最便宜節點D

父節點 節點 耗時 標誌
A B 100元 B已處理
B C -100元 C已處理,但是這邊依舊需要更新了C
C D 2元 D已處理
D E 3元

使用 Dijkstra演算法計算出來結果將是錯誤的A->C->D->E,耗費3元,在 狄克斯拉演算法中被標誌位已處理的節點,便代表中不存在其它到達該節點更便宜的路徑了,由於負權重的引入導致了該原則不成立,上述中由於C節點已經被處理過了,結果又需要再次更新C的內容,但對應的此時C後續的子節點比如D可能存在未同步更新的情況,從而導致最終的結果錯誤。當然也有可能碰巧算對的情況,比如上述案例二中修改A->B = -50的話 , 可以計算出正確的最短路線為A --> B --> E --> H 耗費-37。

個人感覺能不能碰巧算對就看是不是負權重再次更新的節點是不是尚未計算相鄰節點了,比如案例三B優先於C處理:

處理B:

父節點 節點 耗時 標誌
A B 100元 B已處理
A C -100元
.. 終點 ∞當做無窮大

處理C:

父節點 節點 耗時 標誌
A B 100元 B已處理
A C -100元 C已處理
C D -99元
.. 終點 ∞當做無窮大

處理D:

父節點 節點 耗時 標誌
A B 100元 B已處理
A C -100元 C已處理
C D -99元 D已處理
D E -98元

此時正確路徑 A->->C->D->E,倒賺98元

但是這麼做已經和Dijkstra相違背了,在某些情況下可能出現效率極低的現象。所有Dijkstra演算法不應該被用於負權重的情況下。

小結

1.廣度優先演算法BFS主要適用於無權重向圖重搜尋出步驟最少的路徑,當方向圖存在權重時,不再適用

2.狄克斯特拉演算法Dijkstra主要用於有權重的方向圖中搜尋出最短路徑,但不適合於有負權重的情況.對於環圖,個人感覺和BFS一樣,標誌好已處理的節點避免進入死迴圈,可以支援

3.演算法的實現主要都是為了避免沒有更好的解決辦法,而採用窮舉法進行解決,當節點數量極大的情況下,演算法的優勢就會突顯出來

java實現

/**
 * 狄克斯特拉演算法
 * @author Administrator
 *
 */
public class Dijkstra {
	public static void main(String[] args){
		HashMap<String,Integer> A = new HashMap<String,Integer>(){
			{
				put("B",5);
				put("C",1);
			}
		};
		
		HashMap<String,Integer> B = new HashMap<String,Integer>(){
			{
				put("E",10);
			}
		};
		HashMap<String,Integer> C = new HashMap<String,Integer>(){
			{
				put("D",5);
				put("F",6);
			}
		};
		HashMap<String,Integer> D = new HashMap<String,Integer>(){
			{
				put("E",3);
			}
		};
		HashMap<String,Integer> E = new HashMap<String,Integer>(){
			{
				put("H",3);
			}
		};
		HashMap<String,Integer> F = new HashMap<String,Integer>(){
			{
				put("G",2);
			}
		};
		HashMap<String,Integer> G = new HashMap<String,Integer>(){
			{
				put("H",10);
			}
		};
		HashMap<String,HashMap<String,Integer>> allMap = new HashMap<String,HashMap<String,Integer>>() {
			{
				put("A",A);
				put("B",B);
				put("C",C);
				put("D",D);
				put("E",E);
				put("F",F);
				put("G",G);
			}
		};
		
		
		Dijkstra dijkstra = new Dijkstra();
		dijkstra.handle("A","H",allMap);
	}
	
	private String  getMiniCostKey(HashMap<String,Integer> costs,List<String> hasHandleList) {
		int mini = Integer.MAX_VALUE;
		String miniKey = null;
		for(String key : costs.keySet()) {
			if(!hasHandleList.contains(key)) {
				int cost = costs.get(key);
				if(mini > cost) {
					mini = cost;
					miniKey = key;
				}
			}
		}
		return miniKey;
	}
	
	private void handle(String startKey,String target,HashMap<String,HashMap<String,Integer>> all) {
		//存放到各個節點所需要消耗的時間
		HashMap<String,Integer> costMap = new HashMap<String,Integer>();
		//到各個節點對應的父節點
		HashMap<String,String> parentMap = new HashMap<String,String>();
		//存放已處理過的節點key,已處理過的不重複處理
		List<String> hasHandleList = new ArrayList<String>();
		
		//首先獲取開始節點相鄰節點資訊
		HashMap<String,Integer> start = all.get(startKey);
				
		//新增起點到各個相鄰節點所需耗費的時間等資訊
		for(String key:start.keySet()) {
			int cost = start.get(key);
			costMap.put(key, cost);
			parentMap.put(key,startKey);
		}
		
		
		//選擇最"便宜"的節點,這邊即耗費時間最低的
		String minCostKey = getMiniCostKey(costMap,hasHandleList);
		while( minCostKey!=null ) {
			System.out.print("處理節點:"+minCostKey);
			HashMap<String,Integer> nodeMap = all.get(minCostKey);
			if (nodeMap!=null) {
				//該節點沒有子節點可以處理了,末端節點
				handleNode(minCostKey,nodeMap,costMap,parentMap);
			}
			//新增該節點到已處理結束的列表中
			hasHandleList.add(minCostKey);
			//再次獲取下一個最便宜的節點
			minCostKey = getMiniCostKey(costMap,hasHandleList);
		}
		if(parentMap.containsKey(target)) {
			System.out.print("到目標節點"+target+"最低耗費:"+costMap.get(target));
			List<String> pathList = new ArrayList<String>();
			String parentKey = parentMap.get(target);
			while (parentKey!=null) {
				pathList.add(0, parentKey);
				parentKey = parentMap.get(parentKey);
			}
			pathList.add(target);
			String path="";
			for(String key:pathList) {
				path = path + key + " --> ";
			}
			System.out.print("路線為"+path);
		} else {
			System.out.print("不存在到達"+target+"的路徑");
		}
	}
	
	private void handleNode(String startKey,HashMap<String,Integer> nodeMap,HashMap<String,Integer> costMap,HashMap<String,String> parentMap) {
	
		for(String key : nodeMap.keySet()) {
			//獲取原本到父節點所需要花費的時間
			int hasCost = costMap.get(startKey);
			//獲取父節點到子節點所需要花費的時間
			int cost = nodeMap.get(key);
			//計算從最初的起點到該節點所需花費的總時間
			cost = hasCost + cost;
			
			if (!costMap.containsKey(key)) {
				//如果原本並沒有計算過其它節點到該節點的花費
				costMap.put(key,cost);
				parentMap.put(key,startKey);
			}else {
				//獲取原本耗費的時間
				int oldCost = costMap.get(key);
				if (cost < oldCost) {
					//新方案到該節點耗費的時間更少
					//更新到達該節點的父節點和消費時間對應的雜湊表
					costMap.put(key,cost);
					parentMap.put(key,startKey);
					System.out.print("更新節點:"+key + ",cost:" +oldCost + " --> " + cost);
				}
			}
		}
	}
複製程式碼
執行完main方法列印資訊如下:

    處理節點:C
    處理節點:B
    處理節點:D
    更新節點:E,cost:15 --> 9
    處理節點:F
    處理節點:E
    處理節點:G
    處理節點:H
    到目標節點H最低耗費:12路線為A --> C --> D --> E --> H 
複製程式碼

微信公眾號:

演算法(四):圖解狄克斯特拉演算法

相關文章