鍍金池/ 教程/ 數(shù)據(jù)分析&挖掘/ 算法 7:Dijkstra 最短路算法
最快最簡(jiǎn)單的排序——桶排序
算法 7:Dijkstra 最短路算法
算法 2:鄰居好說(shuō)話:冒泡排序
算法 6:只有五行的 Floyd 最短路算法
算法 10:二叉樹
算法 11:堆——神奇的優(yōu)先隊(duì)列(上)
算法 9:開啟“樹”之旅
算法 5:解密回文——棧
算法 4:隊(duì)列——解密 QQ 號(hào)
算法 8:巧妙的鄰接表(數(shù)組實(shí)現(xiàn))
排序總結(jié):小哼買書
算法 3:最常用的排序——快速排序

算法 7:Dijkstra 最短路算法

上周我們介紹了神奇的只有五行的 Floyd 最短路算法,它可以方便的求得任意兩點(diǎn)的最短路徑,這稱為“多源最短路”。本周來(lái)來(lái)介紹指定一個(gè)點(diǎn)(源點(diǎn))到其余各個(gè)頂點(diǎn)的最短路徑,也叫做“單源最短路徑”。例如求下圖中的 1 號(hào)頂點(diǎn)到 2、3、4、5、6 號(hào)頂點(diǎn)的最短路徑。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.1.png" alt="picture7.1" />

與 Floyd-Warshall 算法一樣這里仍然使用二維數(shù)組 e 來(lái)存儲(chǔ)頂點(diǎn)之間邊的關(guān)系,初始值如下。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.2.png" alt="picture7.2" />

我們還需要用一個(gè)一維數(shù)組 dis 來(lái)存儲(chǔ) 1 號(hào)頂點(diǎn)到其余各個(gè)頂點(diǎn)的初始路程,如下。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.3.png" alt="picture7.3" />

我們將此時(shí) dis 數(shù)組中的值稱為最短路的“估計(jì)值”。

既然是求 1 號(hào)頂點(diǎn)到其余各個(gè)頂點(diǎn)的最短路程,那就先找一個(gè)離 1 號(hào)頂點(diǎn)最近的頂點(diǎn)。通過(guò)數(shù)組 dis 可知當(dāng)前離 1 號(hào)頂點(diǎn)最近是 2 號(hào)頂點(diǎn)。當(dāng)選擇了 2 號(hào)頂點(diǎn)后,dis[2]的值就已經(jīng)從“估計(jì)值”變?yōu)榱恕按_定值”,即 1 號(hào)頂點(diǎn)到 2 號(hào)頂點(diǎn)的最短路程就是當(dāng)前 dis[2]值。為什么呢?你想啊,目前離 1 號(hào)頂點(diǎn)最近的是 2 號(hào)頂點(diǎn),并且這個(gè)圖所有的邊都是正數(shù),那么肯定不可能通過(guò)第三個(gè)頂點(diǎn)中轉(zhuǎn),使得 1 號(hào)頂點(diǎn)到 2 號(hào)頂點(diǎn)的路程進(jìn)一步縮短了。因?yàn)?1 號(hào)頂點(diǎn)到其它頂點(diǎn)的路程肯定沒(méi)有 1 號(hào)到 2 號(hào)頂點(diǎn)短,對(duì)吧 O(∩_∩)O~

既然選了 2 號(hào)頂點(diǎn),接下來(lái)再來(lái)看 2 號(hào)頂點(diǎn)有哪些出邊呢。有 2->3 和 2->4 這兩條邊。先討論通過(guò) 2->3 這條邊能否讓 1 號(hào)頂點(diǎn)到 3 號(hào)頂點(diǎn)的路程變短。也就是說(shuō)現(xiàn)在來(lái)比較 dis[3]和 dis[2]+e[2][3]的大小。其中 dis[3]表示 1 號(hào)頂點(diǎn)到 3 號(hào)頂點(diǎn)的路程。dis[2]+e[2][3]中 dis[2]表示 1 號(hào)頂點(diǎn)到 2 號(hào)頂點(diǎn)的路程,e[2][3]表示 2->3 這條邊。所以 dis[2]+e[2][3]就表示從 1 號(hào)頂點(diǎn)先到 2 號(hào)頂點(diǎn),再通過(guò) 2->3 這條邊,到達(dá) 3 號(hào)頂點(diǎn)的路程。

我們發(fā)現(xiàn) dis[3]=12,dis[2]+e[2][3]=1+9=10,dis[3]>dis[2]+e[2][3],因此 dis[3]要更新為 10。這個(gè)過(guò)程有個(gè)專業(yè)術(shù)語(yǔ)叫做“松弛”。即 1 號(hào)頂點(diǎn)到 3 號(hào)頂點(diǎn)的路程即 dis[3],通過(guò) 2->3 這條邊松弛成功。這便是 Dijkstra 算法的主要思想:通過(guò)“邊”來(lái)松弛 1 號(hào)頂點(diǎn)到其余各個(gè)頂點(diǎn)的路程。

同理通過(guò) 2->4(e[2][4]),可以將 dis[4]的值從 ∞ 松弛為 4(dis[4]初始為 ∞,dis[2]+e[2][4]=1+3=4,dis[4]>dis[2]+e[2][4],因此 dis[4]要更新為 4)。

剛才我們對(duì) 2 號(hào)頂點(diǎn)所有的出邊進(jìn)行了松弛。松弛完畢之后 dis 數(shù)組為:

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.4.png" alt="picture7.4" />

接下來(lái),繼續(xù)在剩下的 3、4、5 和 6 號(hào)頂點(diǎn)中,選出離 1 號(hào)頂點(diǎn)最近的頂點(diǎn)。通過(guò)上面更新過(guò) dis 數(shù)組,當(dāng)前離 1 號(hào)頂點(diǎn)最近是 4 號(hào)頂點(diǎn)。此時(shí),dis[4]的值已經(jīng)從“估計(jì)值”變?yōu)榱恕按_定值”。下面繼續(xù)對(duì) 4 號(hào)頂點(diǎn)的所有出邊(4->3,4->5 和 4->6)用剛才的方法進(jìn)行松弛。松弛完畢之后 dis 數(shù)組為:

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.5.png" alt="picture7.5" />

繼續(xù)在剩下的 3、5 和 6 號(hào)頂點(diǎn)中,選出離 1 號(hào)頂點(diǎn)最近的頂點(diǎn),這次選擇 3 號(hào)頂點(diǎn)。此時(shí),dis[3]的值已經(jīng)從“估計(jì)值”變?yōu)榱恕按_定值”。對(duì) 3 號(hào)頂點(diǎn)的所有出邊(3->5)進(jìn)行松弛。松弛完畢之后 dis 數(shù)組為:

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.6.png" alt="picture7.6" />

繼續(xù)在剩下的 5 和 6 號(hào)頂點(diǎn)中,選出離 1 號(hào)頂點(diǎn)最近的頂點(diǎn),這次選擇 5 號(hào)頂點(diǎn)。此時(shí),dis[5]的值已經(jīng)從“估計(jì)值”變?yōu)榱恕按_定值”。對(duì)5號(hào)頂點(diǎn)的所有出邊(5->4)進(jìn)行松弛。松弛完畢之后 dis 數(shù)組為:

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.7.png" alt="picture7.7" />

最后對(duì) 6 號(hào)頂點(diǎn)所有點(diǎn)出邊進(jìn)行松弛。因?yàn)檫@個(gè)例子中 6 號(hào)頂點(diǎn)沒(méi)有出邊,因此不用處理。到此,dis 數(shù)組中所有的值都已經(jīng)從“估計(jì)值”變?yōu)榱恕按_定值”。

最終 dis 數(shù)組如下,這便是 1 號(hào)頂點(diǎn)到其余各個(gè)頂點(diǎn)的最短路徑。

http://wiki.jikexueyuan.com/project/easy-learn-algorithm/images/7.8.png" alt="picture7.8" />

OK,現(xiàn)在來(lái)總結(jié)一下剛才的算法。算法的基本思想是:每次找到離源點(diǎn)(上面例子的源點(diǎn)就是 1 號(hào)頂點(diǎn))最近的一個(gè)頂點(diǎn),然后以該頂點(diǎn)為中心進(jìn)行擴(kuò)展,最終得到源點(diǎn)到其余所有點(diǎn)的最短路徑?;静襟E如下:

  • 將所有的頂點(diǎn)分為兩部分:已知最短路程的頂點(diǎn)集合 P 和未知最短路徑的頂點(diǎn)集合 Q。最開始,已知最短路徑的頂點(diǎn)集合 P 中只有源點(diǎn)一個(gè)頂點(diǎn)。我們這里用一個(gè) book[ i ]數(shù)組來(lái)記錄哪些點(diǎn)在集合 P 中。例如對(duì)于某個(gè)頂點(diǎn) i,如果 book[ i ]為 1 則表示這個(gè)頂點(diǎn)在集合 P 中,如果 book[ i ]為 0 則表示這個(gè)頂點(diǎn)在集合 Q 中。
  • 設(shè)置源點(diǎn) s 到自己的最短路徑為 0 即 dis=0。若存在源點(diǎn)有能直接到達(dá)的頂點(diǎn) i,則把 dis[ i ]設(shè)為 e[s][ i ]。同時(shí)把所有其它(源點(diǎn)不能直接到達(dá)的)頂點(diǎn)的最短路徑為設(shè)為 ∞。
  • 在集合 Q 的所有頂點(diǎn)中選擇一個(gè)離源點(diǎn) s 最近的頂點(diǎn) u(即 dis[u]最小)加入到集合 P。并考察所有以點(diǎn) u 為起點(diǎn)的邊,對(duì)每一條邊進(jìn)行松弛操作。例如存在一條從 u 到 v 的邊,那么可以通過(guò)將邊 u->v 添加到尾部來(lái)拓展一條從 s 到 v 的路徑,這條路徑的長(zhǎng)度是 dis[u]+e[u][v]。如果這個(gè)值比目前已知的 dis[v]的值要小,我們可以用新值來(lái)替代當(dāng)前 dis[v]中的值。
  • 重復(fù)第 3 步,如果集合 Q 為空,算法結(jié)束。最終 dis 數(shù)組中的值就是源點(diǎn)到所有頂點(diǎn)的最短路徑。

完整的 Dijkstra 算法代碼如下:


    #include <stdio.h>
    int main()
    {
        int e[10][10],dis[10],book[10],i,j,n,m,t1,t2,t3,u,v,min;
        int inf=99999999; //用inf(infinity的縮寫)存儲(chǔ)一個(gè)我們認(rèn)為的正無(wú)窮值
        //讀入n和m,n表示頂點(diǎn)個(gè)數(shù),m表示邊的條數(shù)
        scanf("%d %d",&n,&m);

        //初始化
        for(i=1;i<=n;i++)
            for(j=1;j<=n;j++)
                if(i==j) e[i][j]=0;
                  else e[i][j]=inf;

        //讀入邊
        for(i=1;i<=m;i++)
        {
            scanf("%d %d %d",&t1,&t2,&t3);
            e[t1][t2]=t3;
        }
        //初始化dis數(shù)組,這里是1號(hào)頂點(diǎn)到其余各個(gè)頂點(diǎn)的初始路程
        for(i=1;i<=n;i++)
            dis[i]=e[1][i];
        //book數(shù)組初始化
        for(i=1;i<=n;i++)
            book[i]=0;
        book[1]=1;

        //Dijkstra算法核心語(yǔ)句
        for(i=1;i<=n-1;i++)
        {
            //找到離1號(hào)頂點(diǎn)最近的頂點(diǎn)
            min=inf;
            for(j=1;j<=n;j++)
            {
                if(book[j]==0 && dis[j]<min)
                {
                    min=dis[j];
                    u=j;
                }
            }
            book[u]=1;
            for(v=1;v<=n;v++)
            {
                if(e[u][v]<inf)
                {
                    if(dis[v]>dis[u]+e[u][v])
                        dis[v]=dis[u]+e[u][v];
                }
            }
        }

        //輸出最終的結(jié)果
        for(i=1;i<=n;i++)
            printf("%d ",dis[i]);

        getchar();
        getchar();
        return 0;
    }

可以輸入以下數(shù)據(jù)進(jìn)行驗(yàn)證。第一行兩個(gè)整數(shù) n m。n 表示頂點(diǎn)個(gè)數(shù)(頂點(diǎn)編號(hào)為 1~n),m 表示邊的條數(shù)。接下來(lái) m 行表示,每行有 3 個(gè)數(shù) x y z。表示頂點(diǎn) x 到頂點(diǎn) y 邊的權(quán)值為 z。


    6 9
    1 2 1
    1 3 12
    2 3 9
    2 4 3
    3 5 5
    4 3 4
    4 5 13
    4 6 15
    5 6 4

運(yùn)行結(jié)果是


    0 1 8 4 13 17

通過(guò)上面的代碼我們可以看出,這個(gè)算法的時(shí)間復(fù)雜度是 O(N2)。其中每次找到離 1 號(hào)頂點(diǎn)最近的頂點(diǎn)的時(shí)間復(fù)雜度是 O(N),這里我們可以用“堆”(以后再說(shuō))來(lái)優(yōu)化,使得這一部分的時(shí)間復(fù)雜度降低到 O(logN)。另外對(duì)于邊數(shù) M 少于 N2 的稀疏圖來(lái)說(shuō)(我們把 M 遠(yuǎn)小于 N2 的圖稱為稀疏圖,而 M 相對(duì)較大的圖稱為稠密圖),我們可以用鄰接表(這是個(gè)神馬東西?不要著急,下周再仔細(xì)講解)來(lái)代替鄰接矩陣,使得整個(gè)時(shí)間復(fù)雜度優(yōu)化到 O( (M+N)logN )。請(qǐng)注意!在最壞的情況下 M 就是 N2,這樣的話 MlogN 要比 N2 還要大。但是大多數(shù)情況下并不會(huì)有那么多邊,因此(M+N)logN 要比 N2 小很多。

【啊哈!算法】系列 7:Dijkstra 最短路算法
http://ahalei.blog.51cto.com/4767671/1387799