狄克斯特拉算法
一、介紹
在前一篇博客中我們學習了廣度優先搜索算法,它解決的是段數最少的路徑,如果你要找到最快的路徑,該怎么辦呢?為此,可以使用本篇博客所講述的算法——狄克斯特拉算法
如果你使用廣度優先搜索,將得到下面這條段數最少的路徑。
這條路徑耗時7分鐘。下面來看看能否找到耗時更短的路徑!狄克斯特拉算法包含4個步驟。
(1) 找出“最便宜”的節點,即可在最短時間內到達的節點。
(2) 更新該節點的鄰居的開銷,其含義將稍后介紹。
(3) 重復這個過程,直到對圖中的每個節點都這樣做了。
(4) 計算最終路徑。
第一步:找出最便宜的節點。你站在起點,不知道該前往節點A還是前往節點B。前往這兩個節點都要多長時間呢?
前往節點A需要6分鐘,而前往節點B需要2分鐘。至于前往其他節點,你還不知道需要多長時間。
由于你還不知道前往終點需要多長時間,因此你假設為無窮大(這樣做的原因你馬上就會明白)。節點B是最近的——2分鐘就能達到。
第二步:計算經節點B前往其各個鄰居所需的時間。
你剛找到了一條前往節點A的更短路徑!直接前往節點A需要6分鐘。
對于節點B的鄰居,如果找到前往它的更短路徑,就更新其開銷。在這里,你找到了:
- 前往節點A的更短路徑(時間從6分鐘縮短到5分鐘);
- 前往終點的更短路徑(時間從無窮大縮短到7分鐘)。
第三步:重復!
重復第一步:找出可在最短時間內前往的節點。你對節點B執行了第二步,除節點B外,可在最短時間內前往的節點是節點A。
重復第二步:更新節點A的所有鄰居的開銷。
你發現前往終點的時間為6分鐘!
你對每個節點都運行了狄克斯特拉算法(無需對終點這樣做)。現在,你知道:
- 前往節點B需要2分鐘;
- 前往節點A需要5分鐘;
- 前往終點需要6分鐘。
狄克斯特拉算法包含4個步驟。
(1) 找出最便宜的節點,即可在最短時間內前往的節點。
(2) 對于該節點的鄰居,檢查是否有前往它們的更短路徑,如果有,就更新其開銷。
(3) 重復這個過程,直到對圖中的每個節點都這樣做了。
(4) 計算最終路徑。
二、換鋼琴
Rama想拿一本樂譜換架鋼琴。
Alex說:“這是我最喜歡的樂隊Destroyer的海報,我愿意拿它換你的樂譜。如果你再加5美元,還可拿樂譜換我這張稀有的Rick Astley黑膠唱片。”Amy說:“哇,我聽說這張黑膠唱片里有首非常好聽的歌曲,我愿意拿我的吉他或架子鼓換這張海報或黑膠唱片。”
Beethoven驚呼:“我一直想要吉他,我愿意拿我的鋼琴換Amy的吉他或架子鼓。”
太好了!只要再花一點點錢,Rama就能拿樂譜換架鋼琴。現在他需要確定的是,如何花最少的錢實現這個目標。我們來繪制一個圖,列出大家的交換意愿。
這個圖中的節點是大家愿意拿出來交換的東西,邊的權重是交換時需要額外加多少錢。拿海報換吉他需要額外加30美元,拿黑膠唱片換吉他需要額外加15美元。Rama需要確定采用哪種路徑將樂譜換成鋼琴時需要支付的額外費用最少。為此,可以使用狄克斯特拉算法!別忘了,狄克斯特拉算法包含四個步驟。在這個示例中,你將完成所有這些步驟,因此你也將計算最終路徑。動手之前,你需要做些準備工作:創建一個表格,在其中列出每個節點的開銷。這里的開銷指的是達到節點需要額外支付多少錢。
在執行狄克斯特拉算法的過程中,你將不斷更新這個表。為計算最終路徑,還需在這個表中添加表示父節點的列。
第一步:找出最便宜的節點。在這里,換海報最便宜,不需要支付額外的費用。還有更便宜的換海報的途徑嗎?這一點非常重要,你一定要想一想。Rama能夠通過一系列交換得到海報,還能額外得到錢嗎?答案是不能,因為海報是Rama能夠到達的最便宜的節點,沒法再便宜了。
第二步:計算前往該節點的各個鄰居的開銷。
現在的表中包含低音吉他和架子鼓的開銷。這些開銷是用海報交換它們時需要支付的額外費用,因此父節點為海報。這意味著,要到達低音吉他,需要沿從海報出發的邊前行,對架子鼓來說亦如此。
再次執行第一步:下一個最便宜的節點是黑膠唱片——需要額外支付5美元。
再次執行第二步:更新黑膠唱片的各個鄰居的開銷。
你更新了架子鼓和吉他的開銷!這意味著經“黑膠唱片”前往“架子鼓”和“吉他”的開銷更低,因此你將這些樂器的父節點改為黑膠唱片。下一個最便宜的是吉他,因此更新其鄰居的開銷。
你終于計算出了用吉他換鋼琴的開銷,于是你將其父節點設置為吉他。最后,對最后一個節點——架子鼓,做同樣的處理。
如果用架子鼓換鋼琴,Rama需要額外支付的費用更少。因此,采用最便宜的交換路徑時,Rama需要額外支付35美元。
現在來兌現前面的承諾,確定最終的路徑。當前,我們知道最短路徑的開銷為35美元,但如何確定這條路徑呢?為此,先找出鋼琴的父節點。
鋼琴的父節點為架子鼓,這意味著Rama需要用架子鼓來換鋼琴。因此你就沿著這一邊。
我們來看看需要沿哪些邊前行。鋼琴的父節點為架子鼓。
架子鼓的父節點為黑膠唱片。
因此Rama需要用黑膠唱片了換架子鼓。顯然,他需要用樂譜來換黑膠唱片。通過沿父節點回溯,便得到了完整的交換路徑。
三、注意
有向無環圖
圖還可能有環,這意味著你可從一個節點出發,走一圈后又回到這個節點。假設在下面這個帶環的圖中,你要找出從起點到終點的最短路徑。
繞環前行是否合理呢?你可以選擇避開環的路徑。
也可選擇包含環的路徑。
這兩條路徑都可到達終點,但環增加了權重。如果你愿意,甚至可繞環兩次。
但每繞環一次,總權重都增加8。因此,繞環的路徑不可能是最短的路徑。
無向圖意味著兩個節點彼此指向對方,其實就是環!
在無向圖中,每條邊都是一個環。狄克斯特拉算法只適用于有向無環圖。
負權邊
回到上面換鋼琴的例子,假設黑膠唱片不是Alex的,而是Sarah的,且Sarah愿意用黑膠唱片和7美元換海報。換句話說,換得Alex的海報后,Rama用它來換Sarah的黑膠唱片時,不但不用支付額外的費用,還可得7美元。對于這種情況,如何在圖中表示出來呢?
從黑膠唱片到海報的邊的權重為負!即這種交換讓Rama能夠得到7美元。現在,Rama有兩種獲得海報的方式。
第二種方式更劃算——Rama可賺2美元!你可能還記得,Rama可以用海報換架子鼓,但現在有兩種換得架子鼓的方式。
第二種方式的開銷少2美元,他應采取這種方式。然而,如果你對這個圖運行狄克斯特拉算法,Rama將選擇錯誤的路徑——更長的那條路徑。如果有負權邊,就不能使用狄克斯特拉算法。因為負權邊會導致這種算法不管用。下面來看看對這個圖執行狄克斯特拉算法的情況。首先,創建開銷表。
接下來,找出開銷最低的節點,并更新其鄰居的開銷。在這里,開銷最低的節點是海報。根據狄克斯特拉算法,沒有比不支付任何費用獲得海報更便宜的方式。(你知道這并不對!)無論如何,我們來更新其鄰居的開銷。
現在,架子鼓的開銷變成了35美元。
我們來找出最便宜的未處理節點。
更新其鄰居的開銷。
海報節點已處理過,這里卻更新了它的開銷。這是一個危險信號。節點一旦被處理,就意味著沒有前往該節點的更便宜途徑,但你剛才卻找到了前往海報節點的更便宜途徑!架子鼓沒有任何鄰居,因此算法到此結束,最終開銷如下。
換得架子鼓的開銷為35美元。你知道有一種交換方式只需33美元,但狄克斯特拉算法沒有找到。這是因為狄克斯特拉算法這樣假設:對于處理過的海報節點,沒有前往該節點的更短路徑。這種假設僅在沒有負權邊時才成立。因此,不能將狄克斯特拉算法用于包含負權邊的圖。在包含負權邊的圖中,要找出最短路徑,可使用另一種算法——貝爾曼-福德算法,你可以在網上找到其詳盡的說明。
四、實現
有這樣的一條道路,你現在在A點,想要前往H點,請找出最短路徑
想要使用JAVA構造有向圖的數據結構,你可以使用HashMap嵌套,
HashMap<String, HashMap<String, Integer>>外層String代表節點名稱,內層hashmap代表改節點可前往的點,內部hashmap的String代表外層點可到達的點的名稱,Integer代表路程
按照上述步驟書寫代碼,如下:
import java.util.ArrayList; import java.util.HashMap; import java.util.List;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);}}}} }?
總結
- 上一篇: 全国各地迎来降雪,我们准备了五件发热好物
- 下一篇: 自动化测试框架RobotFrameWor