《算法4》最短路径之Dijkstra与Bellman-Ford算法

基本数据结构

在本篇文章中将要记录,在加权有向图中的单源最短路径的两个主要算法,所以首先介绍有向边以及加权有向图这两种关键的数据结构,这里的两种数据结构和《算法4》最小生成树之Prim与Kruskal算法中的边以及加权无向图的数据结构很类似,大致看一下就行。
下面是有向边的数据结构:

public class DirectedEdge {
    private final int v;
    private final int w;
    private final double weight;

    public DirectedEdge(int v, int w, double weight){
        this.v = v;
        this.w = w;
        this.weight = weight;
    }

    public double weight(){
        return weight;
    }

    public  int from (){
        return v;
    }
    public int to(){
        return w;
    }


    public String toString(){
        return String.format("%d->%d %.2f", v,w,weight);
    }


}

下面是加权有向图的数据结构:

public class EdgeWeightedDigraph {
    private final int V;
    private int E;
    private Bag<DirectedEdge>[] adj;

    public EdgeWeightedDigraph(int V){
        this.V = V;
        this.E = 0;
        adj = (Bag<DirectedEdge>[]) new Bag[V];
        for(int v = 0; v<V;v++){
            adj[v] = new Bag<DirectedEdge>();
        }
    }

    public EdgeWeightedDigraph(In in){
        this(in.readInt());
        int E = in.readInt();
        for(int i=0;i<E;i++){
            int v = in.readInt();
            int w = in.readInt();
            Double weight = in.readDouble();
            DirectedEdge e= new DirectedEdge(v, w, weight);
            addEdge(e);
        }
    }

    public int V(){return V;}
    public int E(){return E;}

    public void addEdge(DirectedEdge e){
        int v = e.from();
        adj[v].add(e);
        E++;
    }
    public Iterable<DirectedEdge> adj(int v){
        return adj[v];
    }

    public Iterable<DirectedEdge> edges(){
        Bag<DirectedEdge> bag = new Bag<DirectedEdge>();
        for(int v=0;v<V;v++)
            for (DirectedEdge e:adj[v])
                 bag.add(e);
        return bag;
    }


}

最短路径基本原理

对于一幅加权有向图,从源点开始到所有顶点的所有最短路径构成了一个最短路径树。我们在算法中维护了两个关键的数据结构,edgeTo[]代表在最短路径树中指向每个顶点的边, distTo[]代表从源到某个顶点的“距离”也就是所经过的最短路径边的权重之和。

下面介绍一种关键的技术:“放松”,可以参考下图:
《《算法4》最短路径之Dijkstra与Bellman-Ford算法》

所谓松弛就是对于一条边v->w,此时w到源点的最短路径为distTo[w],这个值不管是不是真的,目前来看是最优值,然后现在如果distTo[v]+e.weight()>distTo[w]那么就证明v->w这条边一定不可能在最短路径树里面,那么这条边就可以不用管了。等于的话我们没必要管它。
如果相反,dist[w]>distTo[v]+e.weoght(),那么就证明我们找到了从源点到达w顶点的一条更短路径,那么就可以更新edgeTo[w]和distTo[w],以上两种情况分别对应于上图左右两种情况。

最短路径的最优性条件:当且仅当对于从v到w的任意一条边e,这些值都满足distTo[w]<=distTo[v]+e.weight()时,他们是最短路径。
具体的证明在此处略过,证明它并不难。下面我们先看两个主要的算法。

Dijkstra算法

其实Dijkstra算法和Prim算法很相似,Prim算法是在横切边中找到权重最小的边加入最小生成树,Dijkstra算法也可以说是在横切边中找权重最小的边。两个算法也都维护了一个索引优先队列,不过一个是存储边的权值,一个存储从源点到该点的路径长度。 所以两个算法完全可以类比,同时加深理解。
对于Dijkstra索引优先队列pq代表在最小路径树之外的还需要进行放松(这里用的为relax()函数)的边,每次从pq中删除路径长度最小的顶点,其实删除就意味着顶点加入最小路径树,因为它不参与之后的比较了。然后将该顶点相连的所有边进行放松,这一步可能会改变某些已经放松过的顶点的路径值,但是这没有关系,首先对于这样的顶点和它相邻的顶点,以前的放松让它满足distTo[w]<=distTo[v]+e.weight(),这一次的放松只会让distTo[w]更小,那么不等式还是满足的。就这样不断delMin(),当索引优先队列为空的时候,所有的边都松弛过了,同时是所有顶点都在最小路径树中了。
下面是代码实现:

public class DijkstraSP {
    private DirectedEdge[] edgeTo;
    private double[] distTo;
    private IndexMinPQ<Double> pq;

    private DijkstraSP(EdgeWeightedDigraph G, int s){
        edgeTo = new DirectedEdge[G.V()];
        distTo = new double[G.V()];
        pq = new IndexMinPQ<Double>(G.V());

        for (int v=0;v<G.V();v++){
            distTo[v] =Double.POSITIVE_INFINITY;
        }
        distTo[s] =0.0;

        pq.insert(s, 0.0);
        while(!pq.isEmpty())
            relax(G, pq.delMin());

    }

    private void relax(EdgeWeightedDigraph G, int v){
        for (DirectedEdge e:G.adj(v)){
            int w = e.to();
            if (distTo[w]>distTo[v]+e.weight()){
                distTo[w] = distTo[v] + e.weight();
                edgeTo[w] = e;
                if (pq.contains(w)) pq.change(w, distTo[w]);
                else                pq.insert(w, distTo[w]);
            }
        }
    }

    public double distTo(int v){
        return distTo[v];
    }
    public boolean hasPathTo(int v){
        return distTo[v]<Double.POSITIVE_INFINITY;
    }

    public Iterable<DirectedEdge> pathTo(int v){
        if (!hasPathTo(v)) return null;
        Stack<DirectedEdge> path =new  Stack<DirectedEdge>();
        for (DirectedEdge edge = edgeTo[v];edge !=null;edge  =edgeTo[edge.from()])
            path.push(edge);
        return path;

    }

}

可以看到代码都和Prim很像,时间成本也是相同的都是 ElogV

但是Dijkstra算法默认边的权重非负,对于有负权重的边,他是不能给出合理的最短路径的,这时候就要用上另一个算法Bellman-Ford算法。

Bellman-Ford算法

Bellman-Ford算法:在任意含有V个顶点的加权有向图中给定起点s,从s无法到达任何负权重环,然后将distTo[s]初始化为0,其他初始化为无穷大,以任意顺序放松所有边,重复V轮。

一个简单的证明可以利用归纳法来证,考虑从s到t的一条最短路径: V0V1Vk , 其中 V0 等于s, Vk 等于t,首先对于平凡情况,即i=0的情况是显然的,然后假设第i次我们得到了从 V0 Vi 的最短路径,那么第i+1次放松之后我们根据前面的最短路径最优性条件,一定有 distTo[Vi+1]dstTo[Vi]+e(ViVi+1).weight() ,然而从起点到 Vi 已经是最短路径了而经过这次放松证明 Vi+1 连接到 Vi 是距离最短的,那么从s到 Vi+1 也是一条最短路径。这种算法比较慢,需要 O(EV) 的时间成本,实际中很浪费。

还有一个重要的问题是如果存在负权重环(就是环的所有边的权重加起来为负),那么经过该环的所有顶点的最短路径都是无意义的,所以算法还需要嫩能够检测负权重环,不过这里的算法和我在《算法4》图&深度优先与广度优先算法这篇文章里面检测无环图部分的思想是一样的,这里就不再赘述。

下面是相关代码:

public class BellmanFordSP {
    private double[] distTo;
    private DirectedEdge[] edgeTo;
    private boolean[] onQ;
    private Queue<Integer> queue;
    private int cost;
    private Iterable<DirectedEdge> cycle;

    public BellmanFordSP(EdgeWeightedDigraph G, int s){
        distTo = new double[G.V()];
        edgeTo = new DirectedEdge[G.V()];
        onQ = new boolean[G.V()];
        queue = new Queue<Integer>();
        for (int v=0;v<G.V();v++)
            distTo[v] = Double.POSITIVE_INFINITY;
        distTo[s] = 0.0;

        queue.enqueue(s);
        onQ[s] = true;
        while(!queue.isEmpty() && !hasNegativeCycle()){
            int v = queue.dequeue();
            onQ [v] =false;
            relax(G, v);
        }

    }

    private void relax(EdgeWeightedDigraph G, int v){
        for (DirectedEdge e:G.adj(v)){
            int w = e.to();
            if(distTo[w] > distTo[v]+e.weight()){
                distTo[w] = distTo[v]+e.weight();
                edgeTo[w] = e;
                if(!onQ[w]){
                    queue.enqueue(w);
                    onQ[w] = true;
                }
            }
            if(cost++ % G.V() ==0){
                findNegativeCycle();
                if (hasNegativeCycle()) return ;
            }
        }
    }

    public double distTo(int v){
        return distTo[v];
    }
    public boolean hasPathTo(int v){
        return distTo[v]<Double.POSITIVE_INFINITY;
    }

    public Iterable<DirectedEdge> pathTo(int v){
        if (!hasPathTo(v)) return null;
        Stack<DirectedEdge> path =new  Stack<DirectedEdge>();
        for (DirectedEdge edge = edgeTo[v];edge !=null;edge  =edgeTo[edge.from()])
            path.push(edge);
        return path;

    }

    private  void findNegativeCycle(){
        int V = edgeTo.length;
        EdgeWeightedDigraph spt=new EdgeWeightedDigraph(V);
        for (int v = 0;v<V; v++)
            if (edgeTo[v] !=null)
                spt.addEdge(edgeTo[v]);
        EdgeWeightedDirectedCycle finderCycle = new EdgeWeightedDirectedCycle(spt);
        cycle =finderCycle.cycle(); 
    }

    public boolean hasNegativeCycle(){
        return cycle != null;
    }

    public Iterable<DirectedEdge> nagativeCycle(){
        return cycle;
    }


}

上面的代码是基于队列的,从其中关键的循环部分while(!queue.isEmpty() && !hasNegativeCycle()), 可以看出,首先我们要一直运行到队列为空,队列什么时候会有顶点入队?当有顶点的distTo[]值被改变的时候,所以当算法运行结束的时候,所有顶点的路径长都已经是最小值,不能再改了。另一种情况是,我们的算法运行了V轮之后,如果有负权重环的话,队列一定是非空的,所以我们每隔V轮检测一下是不是遇到了负权重环,如果遇到了就退出。

可以看出Bellman-Ford其实和Dijstra也有一些相似之处。
下面的代码就是在加权有向图中检测环的代码

public class EdgeWeightedDirectedCycle {
    private boolean[] marked;
    private DirectedEdge[] edgeTo;
    private boolean[] onStack;
    private Stack<DirectedEdge> cycle;

    public EdgeWeightedDirectedCycle(EdgeWeightedDigraph G){
        marked = new boolean[G.V()];
        onStack = new boolean[G.V()];
        edgeTo = new DirectedEdge[G.V()];
        for (int v = 0;v<G.V();v++)
            if (!marked[v]) dfs(G, v);
    }

    private void dfs(EdgeWeightedDigraph G, int v){
        onStack[v] = true;
        marked[v] = true;
        for (DirectedEdge e:G.adj(v)){
            int w = e.to();
            if(cycle !=null) return ;

            else if(!marked[w]){
                edgeTo[w] = e;
                dfs(G, w);
            }
            else if(onStack[w]){
                cycle = new Stack<DirectedEdge>();
                DirectedEdge f = e;
                while(f.from() !=w){
                    cycle.push(f);
                    f = edgeTo[f.from()];
                }
                cycle.push(f);
                return ;
            }
        }
        onStack[v] =false;
    }

    public boolean hasCycle(){
        return cycle!= null;
    }

    public Iterable<DirectedEdge> cycle(){
        return cycle;
    }

}

综上,基于队列的Bellman-Ford算法要比原始版快得多基本是 O(V+E) 的,但是最坏情况也有 O(VE) ,可是它的适用范围要大得多,可以存在负权重环,还可以检测出负权重环。

    原文作者:Bellman - ford算法
    原文地址: https://blog.csdn.net/leonliu1995/article/details/78701997
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞