貝爾曼-福德算法

我在上一篇博客中講解了狄克斯特拉算法,該算法可以用於尋找權值都爲正的有向無環圖的最短路徑,我也提到了如果碰到權值爲負的情況可以使用貝爾曼-福德算法,那麼今天就讓我們學習一下貝爾曼-福德算法是如何處理負權值和環路的。

其實理解了狄克斯特拉算法之後理解貝爾曼-福德算法就很容易了,如果說狄克斯特拉算法是用一種“廣度”的方式,每次尋找源點所能到達的距離最短的一個點,更新其鄰居節點的距離。那麼貝爾曼-福德算法就是用一種“深度”的方式,每次找到所有的路徑,進行“鬆弛”操作最終找到最短路徑。貝爾曼-福德算法每次對所有的邊進行鬆弛,每次鬆弛都會得到一條最短路徑,所以總共需要要做的鬆弛操作是V - 1次。在完成這麼多次鬆弛後如果還是可以鬆弛的話,那麼就意味着,其中包含負環。相比狄克斯特拉算法其時間複雜度較高,達到O(V*E),V代表頂點數,E代表邊數。

你可能不瞭解什麼是鬆弛操作,其實就是不斷尋找距離更短的路徑:

假設有這樣的一個圖

從原點出發經由一條路徑直達A的路徑爲M,直達B的路徑爲N,存在從A到B的路徑L,如果你找到M+L<N的路徑了,那麼就是做了一次鬆弛操作

那麼爲什麼說要做V-1次鬆弛操作呢?

考慮無環圖的極限情況,所有節點排成一條線,那麼最遠的路段是V-1

那麼遇到有環圖呢?

最長路徑便成了無限,但沒經過一次環至少增加兩短路,我們只要經過V-1次鬆弛便可以檢測出來是否有環,對於有環的圖我們還要對其性質進行判斷,如果是正權值的環那麼就排除改路徑,如果是負權值的環那麼此題就無法求解,因爲走環的次數越多權值和越小。

如果你瞭解狄克斯特拉算法的話寫這個也很容易,我們同樣需要三個表一個距離表一個父節點表一個全部節點表,然後就是遍歷V-1次,並判斷是否存在環

我們找一個圖 起始的樣子是這樣的

我們得到了一張從Start開始,第一步只能到達A、B兩點的初始化的距離表,也就是說,此時到達A、B的距離是已知的,因爲可以由Start直接到達; 得到一個只有Start點作爲父節點的兩個點A、B的父地點表;還有一個全部路徑的路徑圖。

我們遍歷路徑表中的 相鄰兩點之間的全部路徑 一次,根據距離表更新每個點的到達距離。也就是對圖進行一次鬆弛操作:

我們可以看到,初始化的時候,Start確定的是隻能直接到達A、B兩點。我們遍歷所有的 兩點之間 的路徑(比如A、C之間的這條 A => C路徑,起點爲:A;終點爲:C;權值爲:2)。我們根據距離表,知道了起點到A的距離是5,到B的距離是3。由於A能到達C、D(存在路徑:A => C 、A => D),所以,此時通過A到達C、D的距離就是(B點同理):

對於經過A直接到達的點:
C的距離 = 5(Start到A的距離)+ 2(A到C的距離,也就是 A => C 的權值)= 7
D的距離 = 5(Start到A的距離)+ 2(A到C的距離,也就是 A => C 的權值)= 7

對於經過B直接到達的點:
C的距離 = 3 (Start到B的距離)+ 1(B到C的距離,也就是 B=> C 的權值)= 4
E的距離 = 3 (Start到B的距離)+ 6(B到E的距離,也就是 B=> C 的權值)= 9

我們依次遍歷所有相鄰兩點之間的路徑,遍歷到 A => C 的時候,我們得到Start到C的距離是 5 + 2 = 7,將其寫入距離表中;但是之後遍歷到 B => C 的時候,我們又得到了Start到C的距離是 3 + 1 = 4 < 7,所以我們更改距離表,到達C的距離就更新爲了4遍歷完所有路徑後(鬆弛之後),我們更新了圖中的距離表,就是現在這個樣子。同樣,我們將記錄進距離表的點的相應的父節點也寫進父地點表。

我們可以對比第一張圖來看,此時我們做的操作,其實就是基於深度(淺色標線)的一次鬆弛操作。

我們再一次遍歷全部的相鄰兩點之間的路徑,我們可以得到如下三張圖:

同樣的操作,我們繼續遍歷所有相鄰兩點之間的路徑(包括之前已經記錄過距離的點,每次都遍歷全部,因爲不知道之前遍歷過的會不會被再次更新,再次鬆弛)。所以這次我們鬆弛的邊分別是(這個圖來說之前兩部遍歷過後,有一些邊再鬆弛權重已經不會改變):

D => C、D => End,C => End、E => End

D => C : 查表得知到D的距離是7,D到C的距離是3,7 + 3 = 10 > 4(表中C的距離),所以C的距離不更新;
D => End: 查表知到D的距離是7,D到End的距離是4,7 + 4 = 11,表中無End,所以加入End = 11, 父地點爲D;
C => End: 查表知到C的距離是4,C到End的距離是5,4 + 5 = 9 < 11(D=>End寫入的),所以更新End的距離爲 9, 父節點爲C;
E => End: 查表知到E的距離是9, E到End的距離是1,9 + 1 = 10 > 9 (C => End更新過的),所以不更新距離表和父節點表。

到此更新完畢了。我們得到了最終的距離表和父地點表。這樣就得到了結論,從Start到到End的最小距離是9,路徑是:End => C(End的父節點) => B(C的父節點) => Start(B的父節點) 的反轉 Start => B => C => End。

求解新問題

具體代碼:

package classic;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class BellmanFord {
    /**
     * 貝爾曼-福德算法
     */
    public static void main(String[] args){

        //創建圖
        Edge ab = new Edge("A", "B", -1);
        Edge ac = new Edge("A", "C", 4);
        Edge bc = new Edge("B", "C", 3);
        Edge be = new Edge("B", "E", 2);
        Edge ed = new Edge("E", "D", -3);
        Edge dc = new Edge("D", "C", 5);
        Edge bd = new Edge("B", "D", 2);
        Edge db = new Edge("D", "B", 1);

        //需要按圖中的步驟步數順序建立數組,否則就是另外一幅圖了,
        //從起點A出發,步驟少的排前面
        Edge[] edges = new Edge[] {ab,ac,bc,be,bd,ed,dc,db};

        //存放到各個節點所需要消耗的時間
        HashMap<String,Integer> costMap = new HashMap<String,Integer>();
        //到各個節點對應的父節點
        HashMap<String,String> parentMap = new HashMap<String,String>();


        //初始化各個節點所消費的,當然也可以再遍歷的時候判斷下是否爲Null
        //i=0的時候
        costMap.put("A", 0); //源點
        costMap.put("B", Integer.MAX_VALUE);
        costMap.put("C", Integer.MAX_VALUE);
        costMap.put("D", Integer.MAX_VALUE);
        costMap.put("E", Integer.MAX_VALUE);

        //進行節點數n-1次循環
        for(int i =1; i< costMap.size();i++) {
            boolean hasChange = false;
            for(int j =0; j< edges.length;j++) {
                Edge edge = edges[j];
                //該邊起點目前總的路徑大小
                int startPointCost = costMap.get(edge.getStartPoint()) == null ? 0:costMap.get(edge.getStartPoint());
                //該邊終點目前總的路徑大小
                int endPointCost = costMap.get(edge.getEndPoint()) == null ? Integer.MAX_VALUE : costMap.get(edge.getEndPoint());
                //如果該邊終點目前的路徑大小 > 該邊起點的路徑大小 + 該邊權重 ,說明有更短的路徑了
                if(endPointCost > (startPointCost + edge.getWeight())) {
                    costMap.put(edge.getEndPoint(), startPointCost + edge.getWeight());
                    parentMap.put(edge.getEndPoint(), edge.getStartPoint());
                    hasChange = true;
                }
            }
            if (!hasChange) {
                //經常還沒達到最大遍歷次數便已經求出解了,此時可以優化爲提前退出循環
                break;
            }
        }

        //在進行一次判斷是否存在負環路
        boolean hasRing = false;
        for(int j =0; j< edges.length;j++) {
            Edge edge = edges[j];
            int startPointCost = costMap.get(edge.getStartPoint()) == null ? 0:costMap.get(edge.getStartPoint());
            int endPointCost = costMap.get(edge.getEndPoint()) == null ? Integer.MAX_VALUE : costMap.get(edge.getEndPoint());
            if(endPointCost > (startPointCost + edge.getWeight())) {
                System.out.print("\n圖中存在負環路,無法求解\n");
                hasRing = true;
                break;
            }
        }

        if(!hasRing) {
            //打印出到各個節點的最短路徑
            for(String key : costMap.keySet()) {
                System.out.print("\n到目標節點"+key+"最低耗費:"+costMap.get(key));
                if(parentMap.containsKey(key)) {
                    List<String> pathList = new ArrayList<String>();
                    String parentKey = parentMap.get(key);
                    while (parentKey!=null) {
                        pathList.add(0, parentKey);
                        parentKey = parentMap.get(parentKey);
                    }
                    pathList.add(key);
                    String path="";
                    for(String k:pathList) {
                        path = path.equals("") ? path : path + " --> ";
                        path = path +  k ;
                    }
                    System.out.print(",路線爲"+path);
                }
            }
        }


    }



    /**
     *  代表"一條邊"的信息對象
     */
    static class Edge{
        //起點id
        private String startPoint;
        //結束點id
        private String endPoint;
        //該邊的權重
        private int weight;
        public Edge(String startPoint,String endPoint,int weight) {
            this.startPoint = startPoint;
            this.endPoint = endPoint;
            this.weight = weight;
        }
        public String getStartPoint() {
            return startPoint;
        }

        public String getEndPoint() {
            return endPoint;
        }

        public int getWeight() {
            return weight;
        }
    }
}

 

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