20-Dijkstra算法

Dijkstra

Dijkstra屬于單源最短路徑算法,用于計算一個頂點到其他所有頂點的最短路徑。

  • 使用前提:不能有負權(quán)邊。也就是說,如果圖中有負權(quán)邊,不能使用Dijkstra算法來計算最短路徑,但是可以使用Bellman-Ford來計算
  • 時間復雜度:可優(yōu)化至O(ElogV),E是邊的數(shù)量,V是頂點數(shù)量。

Dijkstra算法是由荷蘭科學家Edsger Wybe Dijkstra發(fā)明的,也曾在1972年獲得圖靈獎。

Dijkstra等價思考

現(xiàn)在就來研究一下,Dijkstra算法是怎樣的。

首先,Dijkstra的原理其實和生活中的一些自然現(xiàn)象是完全一樣的。什么自然現(xiàn)象呢?接下來就一起跟著下面的描述,來想想一件事情。如果想清楚的話,對于理解Dijkstra算法會大有幫助。

  1. 把圖中的沒一個頂點都想象成為一塊小石頭。
  2. 每一條邊想象成是一條繩子,每一條繩子都連接著2塊小石頭,邊的權(quán)值就是繩子的長度。
  3. 將小石頭和繩子平放在一張桌子上,(下圖是一張俯視圖,圖中黃顏色的是桌子)



    什么是俯視圖?下圖所示的這張圖就是一張俯視圖,所以現(xiàn)在發(fā)揮你的想象力,將上圖黃顏色部分想象成為一張桌面,桌子上面擺了一堆的石頭,然后繩子兩邊連接著石頭


  4. 接下來想象以下,用手拽著小石頭A,慢慢地向上提起來,遠離桌面,如下圖所示的側(cè)視圖


  5. 離開桌面的順序取決于其余頂點距離頂點A的最短繩子長度,也就是最短路徑。所以,最終你會發(fā)現(xiàn),B,D,C,E會依次離開桌面,當所有石頭都離開桌面時,有的繩子會蹦直,而有的繩子是松的。
  6. 最后繃直的繩子就是A到其他小石頭的最短路徑。

關(guān)鍵信息:后離開桌面的小石頭,都是被先離開桌面的小石頭拉起來的。

在本節(jié)中,討論的Dijkstra算法與這種自然現(xiàn)象的原理是非常像的,所以理解了這種自然現(xiàn)象,就對于理解Dijkstra算法有非常大的幫助。

Dijkstra執(zhí)行過程

假設現(xiàn)在有如下的有向圖

現(xiàn)在同樣是計算A到其他頂點之間的最短路徑,模擬剛剛的自然現(xiàn)象的話,就相當于現(xiàn)在要將石頭A拽起來,其他小石頭現(xiàn)在還在桌面,現(xiàn)在要計算A到其他頂點之間的最短路徑,A就是最先被拉起來的小石頭。然后上圖中有不同的顏色,其中黑色表示最短路徑的源頭,紅色表示與剛剛被拉起來的頂點直接相連的頂點,藍色表示不是與剛剛被拉起來的頂點直接相連的頂點。

由于現(xiàn)在A已經(jīng)離開桌面了,所以下一個即將被拽起來的石頭,一定是B,D,E中的其中一個石頭,因為B,D,E是與A直接相連的,C不能再A離開以后的下一個離開桌面,因為B還沒有離開桌面。并且與A直接相連的三個頂點,其中權(quán)值最小的頂點將會成為下一個離開桌面的石頭,被A拽起來。

所以現(xiàn)在由于A已經(jīng)被拽起來,離開了桌面,下一個即將被拽起來的石頭,一定是B,D,E中的其中一個石頭。所以B,D,E是有機會成為下一個離開桌面的石頭。那么在A離開桌面以后,就更新一下A到B,D,E之間的距離,用來表示A到其他頂點之間的最短路徑,更新的表格結(jié)果如下

通過頂點A的outEdges,目前能直接得到的A→B,A→D,A→E的權(quán)值,就如上表所示、由于現(xiàn)在C并沒有與A直接相連,所以A并不知道C的存在,因此A→C的權(quán)值,目前就用∞來進行表示。

通過這個表,就可以確定,下一步哪個石頭即將離開桌面,所以根據(jù)上面的數(shù)據(jù),離A最近的石頭B將成為下一個離開桌面的石頭,離開后的結(jié)果如下

由于B已經(jīng)離開桌面,也就意味著,A到B之間的最短路徑已經(jīng)確定。并且由于B已經(jīng)離開桌面,所以與B直接相連的C也成為了下一個可能即將離開桌面的石頭,那么就可以認為C目前最有可能被B拽起來,所以認為A到C之間的最短路徑可能是A→B→C的路徑,所以更新表格,得到的數(shù)據(jù)如下

  • 綠色:表示已經(jīng)離開桌面,并且是確定的最短路徑,后面不用再考慮這條路徑
  • 紅色:表示更新了的最短路徑信息
  • 藍色:表示本次更新沒有修改

通過這個表格,你可能會想,為什么不通過A→D→C這條路徑來將C拽起來呢?很簡單,因為現(xiàn)在D還沒有離開桌面,所以D目前就沒有能力將C從桌面拽起來,只有B才有可能。最終在下一個離開桌面的石頭中,可能是A拽來的E或者D,也可能是B拽起來的C,至于到底哪個石頭將會被拽起來,則取決于他們與A之間的路徑。

從上表中可以很明顯的看出,沒有離開桌面的C,D,E中,離A最近的是D,所以下一個即將被拽起來的是D,所以D離開桌面以后的結(jié)果如下

由于現(xiàn)在D已經(jīng)離開桌面了,并且發(fā)現(xiàn)D與E,C之間同樣連接著一根線,也就意味著,石頭E和石頭C有新的選擇。

為什么呢?就拿E來說,在D沒有被拽起來離開桌面之前,E只可能被A拽起來離開桌面,而現(xiàn)在D被拽起來了以后,E就可能既被A拽起來離開桌面,也可能被D拽起來離開桌面,至于最終被誰拽起來,則取決于A→E之間的權(quán)重與A→D→E之間的權(quán)重誰更小。

根據(jù)上面的解釋,也就意味著E和C均多了一種被D拽起來離開桌面的可能。所以當D被拽起來離開桌面以后,就更新A到C,A到E之間的路徑長度,所以更新后的表格如下

接下來,哪個石頭將被拽起來,同樣是依據(jù)表格中還沒有被拽起來石頭中個,哪個石頭到原點A之間的距離最小,最小的就將被拽起來。很明顯,下一個將被拉起來的石頭是C,拽起來以后的結(jié)果如下

到現(xiàn)在,A,B,C,D都被拽起來了,而且請注意,一旦C被拽起來,也就意味著E又多了一種新的選擇。因為在C被拽起來之前,認為E可能是通過A→D→E這條路徑,將E拽起來的。但是現(xiàn)在C也被拽起來了,所以E有可能通過A→D→C→E這條路徑被拽起來。所以就比較這兩條路徑,哪條路徑的路徑長度更短,最終比較發(fā)現(xiàn)是通過A→D→C→E這條路徑是更短的,所以更新表格中的路徑長度

由于到現(xiàn)在,沒有被拽起來的石頭只有E,所以下一個被拽起來的石頭就是E,并且通過A→D→C→E這條路徑被轉(zhuǎn)起來的。

最終,下圖中綠色的線,就是最終被繃直的線

最短路徑對應的表格如下

以上的這個過程,就是通過Dijkstra算法計算最短路徑的流程。

松弛操作(Relaxation)

松弛操作:表示更新兩個頂點之間最短路徑

例如前面D被拉起來以后,C和E都多了一種被拉起來的選擇,就是E可能被A拉起來,也可能被D拉起來;C可能被D拉起來,也可能被B拉起來。具體在D被來起來以后,C和E將被哪個石頭拉起來,就需要更新C到A之間的路徑,更新E到A之間的路徑。這種更新兩個頂點之間的路徑的操作,就叫做松弛操作。也就是說,確定A到D的最短路徑以后,對DC,DE邊進行松弛操作,更新了A到C,A到E的最短路徑。

在這里的松弛操作,一般指的是,更新源點到另一個點之間的最短路徑。

所以前面的過程,通過松弛操作來進行闡述的話,就是

  1. 一旦有石頭被拽起來,就需要對該石頭的outEdges進行松弛操作,計算出outEdges這些頂點被拽起來的最短路徑。
  2. 所以當A被拽起來以后,就需要對AB,AD,AE這些邊進行松弛操作,更新這些邊到原點之間的最短路徑
  3. 找出路徑長度最短的點,通過上面的表格可以知道,當前路徑最短的是A→B這條路徑,所以B被拽起來
  4. B一旦被拽起來,也就意味著A到B之間的最短路徑確定了,然后對B的outEdges進行松弛操作,由于目前B的outEdges只有一條表BC,所以現(xiàn)在對BC邊進行松弛操作
  5. 基礎超出路徑長度最小的頂點,所以這一次,D被拽了起來
  6. D被拽起來了以后,又會對D的outEdges進行一次松弛操作,所以會對DE,DC這兩條邊進行松弛操作
  7. 就依照這種流程,一直重復,直到確定所有頂點的最短路徑,算法就結(jié)束了。

Dijkstra實現(xiàn)

結(jié)合前面的分析,最終要實現(xiàn)的是計算出某個頂點到其他頂點的最短路徑。

其中要計算出最短路徑,則有以下幾個關(guān)鍵步驟:

  1. 每當有一個頂點被拽起來以后,就會從對路徑發(fā)生變化的頂點進行松弛操作,在不斷松弛的過程中,會更新該頂點到其他頂點之間的最短路徑長度
  2. 選擇一個路徑長度最短的頂點,作為下一個即將離開桌面的頂點
  3. 重復以上兩個步驟

所以結(jié)合這個思路,可以得到計算最短路徑的大概框架

public Map<V, E> shortestPath(V begin) {
    Vertex beginVertex = vertices.get(begin);
    if (beginVertex == null) return null;
    Map<V, E> selectedPaths = new HashMap<>();
    Map<Vertex<V,E>, E> paths = new HashMap<>();
    while (!paths.isEmpty()) {
        Map.Entry<Vertex<V,E>,E> minEntry = getShortestPath(paths);
        //minEntry離開桌面
        Vertex<V,E> minVertex = minEntry.getKey();
        selectedPaths.put(minVertex.value,minEntry.getValue());
        paths.remove(minVertex);
        //對minVertex的outEdges進行松弛操作
        for (Edge<V,E> edge : minVertex.outEdges) {
            relax();
        }
    }
    return selectedPaths;
}

/*
* 從paths中選出一個最短的路徑出來
* */
private Map.Entry<Vertex<V,E>,E> getShortestPath(Map<Vertex<V,E>, E> paths) {
    return null;
}
/*
* 進行松弛操作
* */
private void relax() {

}

其中selectedPaths這個Map用來保存已經(jīng)計算出最短路徑的頂點,paths這個Map用來保存下一個即將離開桌面的頂點。當paths中的所有頂點都計算出最短路徑以后,說明整個最短路徑就計算完畢了。

結(jié)合上面的框架,與前面的分析思路,最終得到的基礎版本實現(xiàn)如下

public Map<V, E> shortestPath(V begin) {
    Vertex<V,E> beginVertex = vertices.get(begin);
    if (beginVertex == null) return null;
    Map<V, E> selectedPaths = new HashMap<>();
    Map<Vertex<V,E>, E> paths = new HashMap<>();
    //初始化paths
    for (Edge<V,E> edge: beginVertex.outEdges) {
        paths.put(edge.to,edge.weight);
    }
    while (!paths.isEmpty()) {
        Map.Entry<Vertex<V,E>,E> minEntry = getShortestPath(paths);
        //minEntry離開桌面
        Vertex<V,E> minVertex = minEntry.getKey();
        selectedPaths.put(minVertex.value,minEntry.getValue());
        paths.remove(minVertex);
        //對minVertex的outEdges進行松弛操作
        for (Edge<V,E> edge : minVertex.outEdges) {
            //如果edge.to已經(jīng)離開桌面,就沒必要進行松弛操作
            if (selectedPaths.containsKey(edge.to.value)) continue;;
            //relax();
            //新的可選的最短路徑:beginVertex到edge.from的最短路徑 + edge.weight
            //minEntry.getValue() + edge.weight;
            E newWeight = weightManager.add(minEntry.getValue(),edge.weight);
            //以前的最短路徑:beginVertex到edge.to的最短路徑
            E oldWeight = paths.get(edge.to);
            //比較兩個路徑長度
            if (oldWeight == null || weightManager.compare(newWeight,oldWeight) < 0) {
                //如果新的路徑長度比舊的路徑更短,或者原來沒有舊的路徑長度,就更新路徑長度
                paths.put(edge.to,newWeight);
            }
        }
    }
    //如果是無向圖,前面會將起點也添加進去,所以最后將起點刪除掉
    selectedPaths.remove(begin);
    return selectedPaths;
}

/*
* 從paths中選出一個最短的路徑出來
* */
private Map.Entry<Vertex<V,E>,E> getShortestPath(Map<Vertex<V,E>, E> paths) {
    Iterator<Map.Entry<Vertex<V,E>,E>> it = paths.entrySet().iterator();
    Map.Entry<Vertex<V,E>,E> minEntry = it.next();
    while (it.hasNext()) {
        Map.Entry<Vertex<V,E>,E> entry = it.next();
        if (weightManager.compare(entry.getValue(),minEntry.getValue()) < 0) {
            minEntry = entry;
        }
    }
    return minEntry;
}

所以上面的代碼,就是完完全全按照前面的分析,結(jié)合將小石頭從桌子上提起來這種生活現(xiàn)象,對Dijkstra這種單源最短路徑算法進行實現(xiàn)。所以可以發(fā)現(xiàn)整體的思路還是挺簡單的,而且代碼也不復雜。

但是目前上面這種算法,返回的內(nèi)容是終點與原點之間,對應的路徑長度。并沒有包含是通過哪些頂點計算出來的最短路徑。所以有時候可能希望返回的數(shù)據(jù)里面,包含了從原點到達終點時,經(jīng)過了哪些頂點。所以需要將上面的算法進行改進

Dijkstra改進

為了達到前面想要實現(xiàn)的功能,所以需要對返回的數(shù)據(jù)進行修改。在返回的Map中,將經(jīng)過的最短路徑也保存到Map中,這樣返回的結(jié)果就跟全面了。

首先要改進的地方是對接口進行修改。在前面的Map中,每一個鍵值對中,key(頂點)對應的value是權(quán)值,所以現(xiàn)在要將頂點對應的信息修改為邊的信息+ 權(quán)值,這樣就能拿到最終計算出最短路徑時,經(jīng)過的路徑信息。

最終經(jīng)過改進后的算法如下:

public Map<V, PathInfo<V, E>> shortestPath(V begin) {
    Vertex<V,E> beginVertex = vertices.get(begin);
    if (beginVertex == null) return null;
    Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>();
    Map<Vertex<V,E>, PathInfo<V, E>> paths = new HashMap<>();
    //初始化paths
    for (Edge<V,E> edge: beginVertex.outEdges) {
        PathInfo<V,E> path = new PathInfo<V,E>();
        path.setWeight(edge.weight);
        path.edgeInfos.add(edge.info());
        paths.put(edge.to,path);
    }
    while (!paths.isEmpty()) {
        Map.Entry<Vertex<V,E>, PathInfo<V,E>> minEntry = getShortestPath(paths);
        //minEntry離開桌面
        Vertex<V,E> minVertex = minEntry.getKey();
        selectedPaths.put(minVertex.value,minEntry.getValue());
        paths.remove(minVertex);
        //對minVertex的outEdges進行松弛操作
        for (Edge<V,E> edge : minVertex.outEdges) {
            //如果edge.to已經(jīng)離開桌面,就沒必要進行松弛操作
            if (selectedPaths.containsKey(edge.to.value)) continue;;
            //relax();
            //新的可選的最短路徑:beginVertex到edge.from的最短路徑 + edge.weight
            //minEntry.getValue() + edge.weight;
            E newWeight = weightManager.add(minEntry.getValue().weight,edge.weight);
            //以前的最短路徑:beginVertex到edge.to的最短路徑
            PathInfo<V,E> oldPath = paths.get(edge.to);
            //比較兩個路徑長度
            if (oldPath == null || weightManager.compare(newWeight,oldPath.weight) < 0) {
                //如果新的路徑長度比舊的路徑更短,或者原來沒有舊的路徑長度,就更新路徑長度
                PathInfo<V,E> path = new PathInfo<>();
                path.weight = newWeight;
                path.edgeInfos.addAll(minEntry.getValue().edgeInfos);
                path.edgeInfos.add(edge.info());
                paths.put(edge.to,path);
            }
        }
    }
    //如果是無向圖,前面會將起點也添加進去,所以最后將起點刪除掉
    selectedPaths.remove(begin);
    return selectedPaths;
}

/*
* 從paths中選出一個最短的路徑出來
* */
private Map.Entry<Vertex<V,E>, PathInfo<V,E>> getShortestPath(Map<Vertex<V,E>, PathInfo<V,E>> paths) {
    Iterator<Map.Entry<Vertex<V,E>, PathInfo<V,E>>> it = paths.entrySet().iterator();
    Map.Entry<Vertex<V,E>, PathInfo<V,E>> minEntry = it.next();
    while (it.hasNext()) {
        Map.Entry<Vertex<V,E>, PathInfo<V,E>> entry = it.next();
        if (weightManager.compare(entry.getValue().weight,minEntry.getValue().weight) < 0) {
            minEntry = entry;
        }
    }
    return minEntry;
}

然后將以上代碼進行優(yōu)化后,結(jié)果如下

public Map<V, PathInfo<V, E>> shortestPath(V begin) {
    Vertex<V,E> beginVertex = vertices.get(begin);
    if (beginVertex == null) return null;
    Map<V, PathInfo<V, E>> selectedPaths = new HashMap<>();
    Map<Vertex<V,E>, PathInfo<V, E>> paths = new HashMap<>();
    //初始化paths
    for (Edge<V,E> edge: beginVertex.outEdges) {
        PathInfo<V,E> path = new PathInfo<V,E>();
        path.setWeight(edge.weight);
        path.edgeInfos.add(edge.info());
        paths.put(edge.to,path);
    }
    while (!paths.isEmpty()) {
        Map.Entry<Vertex<V,E>, PathInfo<V,E>> minEntry = getShortestPath(paths);
        //minEntry離開桌面
        Vertex<V,E> minVertex = minEntry.getKey();
        PathInfo<V,E> minPath = minEntry.getValue();
        selectedPaths.put(minVertex.value,minPath);
        paths.remove(minVertex);
        //對minVertex的outEdges進行松弛操作
        for (Edge<V,E> edge : minVertex.outEdges) {
            //如果edge.to已經(jīng)離開桌面,就沒必要進行松弛操作
            if (selectedPaths.containsKey(edge.to.value)) continue;;
            relax(edge,paths,minPath);
        }
    }
    //如果是無向圖,前面會將起點也添加進去,所以最后將起點刪除掉
    selectedPaths.remove(begin);
    return selectedPaths;
}

/*
* 從paths中選出一個最短的路徑出來
* */
private Map.Entry<Vertex<V,E>, PathInfo<V,E>> getShortestPath(Map<Vertex<V,E>, PathInfo<V,E>> paths) {
    Iterator<Map.Entry<Vertex<V,E>, PathInfo<V,E>>> it = paths.entrySet().iterator();
    Map.Entry<Vertex<V,E>, PathInfo<V,E>> minEntry = it.next();
    while (it.hasNext()) {
        Map.Entry<Vertex<V,E>, PathInfo<V,E>> entry = it.next();
        if (weightManager.compare(entry.getValue().weight,minEntry.getValue().weight) < 0) {
            minEntry = entry;
        }
    }
    return minEntry;
}

private void relax(Edge<V,E> edge, Map<Vertex<V,E> ,PathInfo<V,E>> paths, PathInfo<V,E> fromPath) {
    //新的可選的最短路徑:beginVertex到edge.from的最短路徑 + edge.weight
    //minEntry.getValue() + edge.weight;
    E newWeight = weightManager.add(fromPath.weight,edge.weight);
    //以前的最短路徑:beginVertex到edge.to的最短路徑
    PathInfo<V,E> oldPath = paths.get(edge.to);
    //如果新的路徑大于等于原來路徑,就不用做任何操作
    if (oldPath != null && weightManager.compare(newWeight,oldPath.weight) >= 0) return;
    if (oldPath == null) {
        oldPath = new PathInfo<>();
        paths.put(edge.to,oldPath);
    }else {
        oldPath.edgeInfos.clear();
    }
    oldPath.weight = newWeight;
    oldPath.edgeInfos.addAll(fromPath.edgeInfos);
    oldPath.edgeInfos.add(edge.info());
    paths.put(edge.to,oldPath);
}

到這里,就將Dijkstra計算原點到其他點的最短路徑計算出來了。不過這種算法并不是最優(yōu)的算法,因為在從paths中選出一個最短的路徑出來時,使用的是便于理解的遍歷比較來實現(xiàn)的,這種算法,效率比較低,在前面有介紹過使用堆這種數(shù)據(jù)結(jié)構(gòu)來獲取最值,效率非常高。所以如果要優(yōu)化的話, 可以考慮在從paths中選出一個最短的路徑出來時,使用最小堆這種數(shù)據(jù)結(jié)構(gòu)來實現(xiàn)。

demo下載地址

完!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

友情鏈接更多精彩內(nèi)容