贝尔曼-福德算法

我在上一篇博客中讲解了狄克斯特拉算法,该算法可以用于寻找权值都为正的有向无环图的最短路径,我也提到了如果碰到权值为负的情况可以使用贝尔曼-福德算法,那么今天就让我们学习一下贝尔曼-福德算法是如何处理负权值和环路的。

其实理解了狄克斯特拉算法之后理解贝尔曼-福德算法就很容易了,如果说狄克斯特拉算法是用一种“广度”的方式,每次寻找源点所能到达的距离最短的一个点,更新其邻居节点的距离。那么贝尔曼-福德算法就是用一种“深度”的方式,每次找到所有的路径,进行“松弛”操作最终找到最短路径。贝尔曼-福德算法每次对所有的边进行松弛,每次松弛都会得到一条最短路径,所以总共需要要做的松弛操作是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;
        }
    }
}

 

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