最短路徑算法:Bellman-ford算法

最短路徑問題

在圖結構中,求解最短路徑問題有多種算法,Bellman-Ford是其中之一,它可以處理含有負權邊的情況,同樣是單源最短路徑算法,而之前講到的Dijkstra算法不能處理含有負權邊的情況。對應的代價就是其算法時間複雜度要高一些。後面我們會分析。

這裏能處理負權邊是針對有向圖的,因爲對無向圖來說,含有負權邊就意味着含有負權迴路,在有負權迴路的這種情況下求最短路徑是無解的,因爲每經過一次負權迴路,距離都會減少,就會無限循環下去。

在繼續往下講之前,先補充一個圖最短路徑的一個性質:最短路徑的子路徑也是最短路徑,數學描述如下:有向圖G=(V,E)G=(V,E),設p=(v0,v1,...,vk)p=(v_0,v_1,...,v_k)爲從點v0v_0到點vkv_k的一條最短路徑,且0ijk0≤i≤j≤k,設pij=(vi,vi+1,...,vj)p_{ij}=(v_i,v_{i+1},...,v_j)爲路徑pp中從點viv_i到點vjv_j的子路徑,那麼pijp_{ij}也是這兩點之間的一條最短路徑。可用反證法來證明:

證明:如果將路徑pp分解爲v0vivjvkv_0-v_i-v_j-vk,則有w(p)=w(p0i)+w(pij)+w(pjk)w(p)=w(p_{0i})+w(p_{ij})+w(p_{jk})。假設存在一條從viv_ivjv_j的一條更短的路徑pijp_{ij}'w(pij<w(pij))w(p_{ij}'<w(p_ij))。則新路徑權值爲w(p0i)+w(pij)+w(pjk)<w(p)w(p_{0i})+w(p_{ij}')+w(p_{jk}) < w(p),這與pp是最短路徑相矛盾。

Bellman-Ford算法

與Dijkstra算法類似,Bellman-Ford算法也是通過不斷的“鬆弛”操作來求得最終解。“鬆弛”就是如下的操作過程:w(u,v)w(u,v)表示uuvv之間的權值,d[v]d[v]表示從源點ss到頂點vv的距離,若存在邊e(u,v)e(u,v),使得:d[v]>d[u]+w(u,v)d[v] > d[u] + w(u,v)(即發現了優於當前的路徑),則更新d[v]=d[u]+w(u,v)d[v] = d[u] + w(u,v),並更新路徑prev[v]=uprev[v] = u。可以看到每一次“鬆弛”都會更逼近最優解。Dijkstra算法通過優先隊列每次選擇當前未被處理過的距離最小的頂點,對該頂點未被處理過的邊進行鬆弛。而Bellman-Ford算法則簡單的鬆弛所有的邊,反覆執行V1|V|-1次(V|V|爲頂點的的個數),時間複雜度O(VE)O(|V||E|)。可以看出,Bellman-Ford鬆弛的次數遠多於Dijkstra,所以其時間複雜度相比Dijkstra要高。

僞代碼如下:

function BellmanFord(list vertices, list edges, vertex source)
    // step 1 初始化, dist[v]表示源節點到頂點v的距離值,prev[v]表示頂點v的前驅頂點
    for each vertex v in vertices
        dist[v] = inf
        prev[v] = null

    dist[source] = 0

    // step 2 迭代鬆弛|V|-1次
    for i from 1 to size(vertices) -1 
        for each edge(u,v) with weight(u,v)  in edges
            if dist[u] + weight(u,v) < dist[v]
                dist[v] = dist[u] + weight(u,v)
                prev[v] = u
    
    // step 3 檢查是否有負權迴路
    for each edge(u,v) with weight(u,v) in edges
        if dist[u] + weight(u,v) < dist[v]
            error "檢測到負權迴路"

    return dist[], prev[]

對算法的優化: 在實際應用中,Bellman-Ford算法其實不用迭代鬆弛V1|V|-1次,理論上圖中存在的最大的路徑長度爲V1|V|-1,實際上往往要小於這個V1|V|-1,即,在V1|V|-1次迭代鬆弛之前就已經收斂了,計算出最短路徑了,所以可在循環中設置判定,在某次循環中不再進行鬆弛時,表明當前已收斂,可退出步驟2,進行下一步檢查是否有負權迴路。

怎麼理解這個算法呢? 假設某頂點與源頂點沒有連通,即沒有邊,那麼這個點就不會被鬆弛,距離不會被更新,依舊爲無窮大。如果頂點與源頂點是連通的,在不存在負權迴路的情況下,一定存在一條最短路徑,這條最短路徑p=(v0,v1,...,vk)p=(v_0,v_1,...,v_k)爲源點ssvv之間的任意一條最短路徑(這裏v0=sv_0=svk=vv_k=v)。最大會有多少條邊呢?假設圖有V|V|個頂點,那麼有kV1k≤|V|-1。在進行第一輪鬆弛時,被鬆弛的邊中一定會包含邊(v0,v1)(v_0,v_1),結合文章開頭講到的最短路徑的子路徑也一定是最短路徑的性質,v1v_1已經得到了其最短路徑,在第二輪鬆弛過程中,被鬆弛的邊中一定會包含(v1,v2)邊(v_1,v_2),經過此次鬆弛後,v2v_2也已經得到了其最短路徑。以此類推,在第kk輪鬆弛中,被鬆弛的邊中一定包含了邊(vk1,vk)(v_{k-1},v_k),之後vkv_k也得到其最短路徑。也就是說,凡是與源頂點最短路徑經過的邊數爲kk的頂點,在第kk輪鬆弛時一定會被確認(最短路徑被找到)。所以,我們需要鬆弛多少輪呢,最多V1|V|-1次就可以了。

算法的數學證明可以參考《圖論》或《算法導論》中的證明過程。

代碼實現見bellman_ford.cpp。最後再分析一下時間複雜度,最壞的情況O(VE)O(|V||E|),這個比較好理解,最好的情況O(E)O(|E|),一次鬆弛所有邊的操作就可以了,對應的就是邊鬆弛的順序恰好是最短路徑樹的生成順序。

算法的應用

其中一個應用就是路由協議了(距離向量協議),對此實現了一個路由協議測試工程,代碼見router。實現了一個通過路由表的方式進行的路由算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章