数据结构笔记_最短路径(有向图,无向图)

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/jt102605/article/details/84727198

一. 最短路径问题

  • 对无权图来说,进行广度优先遍历的过程,其实就求出了从一个节点开始到它所有可到达节点的最短路径
  • 对无权图进行广度优先遍历,最终会形成一棵生成树,称为最短路径树(相当于有权图中的最小生成树),即解决了无权图单源最短路径问题(求从一个节点到其它所有可到达节点的最短路径)

  • 有权图的最短路径问题,核心思想为“松弛操作(Relaxation)”, 即判断中间经过另一个节点到达某节点是否比直接到达该节点所花费的权重更小

二. dijkstra单源最短路径法

  • 前提:图中不能含有负权边 (保证某个节点的所有相邻节点中路径最短的那个相邻节点就是最短路径上的节点,因为若通过另外的相邻节点"松弛"到达该节点,所花费的路径一定更长;如上图中,0—>2 是节点0到达所有的邻节点的路径中权重最小的路径,0—>2一定是最短路径, 因为若经过节点5"松弛"到达节点2,在没有“负权边”(1—>2)的情况下,路径一定会变长)
  • 时间复杂度:O(ElogV)
//dijkstra 求无负权值图最短路径  辅助数据结构:最小索引堆
public class Dijkstra <Weight extends Number & Comparable>{
    private WeightGraph<Weight> graph;   //待求解的图
    private int s;               //源点
    private boolean[] isVisited; //标记节点是否被访问
    private Number[] distTo;     //记录每个节点到源点的路径
    private Edge<Weight>[] from; // from[i]记录最短路径中, 到达i点的边是哪一条 可以用来恢复整个最短路径

    public Dijkstra(WeightGraph graph, int s){
        this.graph = graph;
        this.s = s;
        this.distTo = new Number[graph.V()];
        this.from = (Edge<Weight>[]) new Edge[graph.V()];
        this.isVisited = new boolean[graph.V()];
        for(int i=0; i<graph.V(); i++){
            distTo[i] = 0.0;
            from[i] = null;
            isVisited[i] = false;
        }
    }

    //dijkstra
    public void dijkstra(){
        IndexMinHeap<Weight> heap = new IndexMinHeap<>(graph.V(), true);

        //初始化,
        distTo[s] = 0.0;
        from[s] = new Edge<Weight>(s,s,(Weight)(Number) 0.0);
        heap.add(s, (Weight) distTo[s]);

        while(!heap.isEmpty()){

            //以出堆节点为“中间点”对出堆点的为被标记过的邻节点作“松弛”
            //松弛后索引堆中的最小路径所对应的节点就是下次要出堆的节点,即该节点的最短路径已经找到
            int v = heap.removeIndex();
            isVisited[v] = true;

            for(Edge<Weight> e: graph.adjIterator(v)){  //e为
                int w = e.otherV(v);
                //找出出堆节点未被标记的邻结点,对其做“松弛“
                if(!isVisited[w]){
                    //"松弛"分两类,以前没有访问过, 或者访问过, 但是通过当前的v点到w点距离更短, 则进行更新
                    //注:标记指isVisited[i] = true,是出堆时的操作, 访问指from[i]!=null
                    if(from[w] == null || distTo[v].doubleValue()+e.weight().doubleValue()<distTo[w].doubleValue()){
                        distTo[w] = distTo[v].doubleValue()+e.weight().doubleValue();
                        from[w] = e;
                        if(heap.contain(w)){
                            heap.set(w, (Weight) distTo[w]);
                        }else{
                            heap.add(w, (Weight)distTo[w]);
                        }
                    }
                }
            }
        }
    }


    // 判断从s点到v点是否联通
    public boolean hasPath(int v){
        assert v>=0 && v<graph.V();
        return isVisited[v];
    }

    // 返回从s点到v点的最短路径长度
    public Number shortestPathTo(int v){
        assert v >= 0 && v < graph.V();
        assert hasPath(v);
        return distTo[v];
    }

    // 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
    private Vector<Edge<Weight>> shortestPath(int w){
        assert w >= 0 && w < graph.V();
        assert hasPath(w);

        // 通过from数组逆向查找到从s到w的路径, 存放到栈中
        Stack<Edge<Weight>> stack = new Stack<>();
        while (from[w].otherV(w) != s){
            stack.push(from[w]);
            w = from[w].otherV(w);
        }
        stack.push(from[s]);

        // 从栈中依次取出元素, 获得顺序的从s到w的路径
        Vector<Edge<Weight>> vector = new Vector<>();
        while (!vector.isEmpty()){
            vector.add(stack.pop());
        }

        return vector;
    }

    //打印出从s点到w点的路径
    public void showPath(int w){
        assert w >= 0 && w < graph.V();
        assert hasPath(w);

        Vector<Edge<Weight>> vector = shortestPath(w);

        for (int i=0; i<vector.size(); i++){
            System.out.print( vector.elementAt(i).v() + " -> ");
            if( i == vector.size()-1 )
                System.out.println(vector.elementAt(i).w());
        }
    }
}

三. Bellman-Ford算法

  • 负权环问题:从源点到任何一点,只要能经过负权环,则会不停的在负权环中转,因为每经过一次负权环所得到的总费用就会跟小,从而导致图中不存在最短路径(注:两个节点之间也可以形成负权环)

  • 前提:图中可以有负权边,但不能有负权环,因为基于“松弛”操作的最短路径算法在负权环存在的情况下,将不存在最短路径。
  • Bellman-Ford算法可以用来判断图中是否存在负权环
  • 复杂度:O(EV)
  • 算法思想:
  1. 如果一个图没有负权环,则从源点到另外一点的最短路径最多经过所有的V个顶点和V-1条边,否则,存在顶点经过两次的情况,即出现了负权环;
  2. 对一个点的松弛操作,就是找到经过这个点的另外一条路径,多经过一条边使得权值更小;如果一个图没有负权环,则从源点到另外一点的最短路径最多经过所有的V个顶点V-1条边;所以,对所有的节点进行V-1次松弛操作,理论上就找到了从源点到其它所有节点的最短路径,若还可以继续松弛,说明原图中存在负权环。  
  • Bellman-Ford的优化:利用队列数据结构可以优化Bellman-Ford算法
public class BellmanFord<Weight extends Number & Comparable> {
    private WeightGraph<Weight> graph;
    private int s;
    private Number[] distTo;
    private Edge<Weight>[] from;
    private boolean hasNegativeCycle;

    public BellmanFord(WeightGraph graph, int s){
        this.graph = graph;
        this.s = s;
        this.distTo = (Weight[]) new Number[graph.V()];
        this.from = (Edge<Weight>[]) new Edge[graph.V()];
        this.hasNegativeCycle = false;

        for (int i=0; i<graph.V(); i++){
            from[i] = null;
        }
    }

    //bellman-ford
    public void bellmanFord(){
        //对源节点初始化
        from[s] = new Edge<Weight>(s, s, (Weight)(Number) 0.0);
        distTo[s] = (Weight)(Number) 0.0;

        //对图中所有点进行“邻边绕道”的松弛操作,这个松弛操作进行了V-1轮
        for(int pass=1; pass<graph.V(); pass++){

            //对所有的节点,进行松弛
            // 每次循环中对所有的边进行一遍松弛操作
            // 遍历所有边的方式是先遍历所有的顶点, 然后遍历和所有顶点相邻的所有边
            for (int i=0; i<graph.V(); i++){
                for(Edge<Weight> e: graph.adjIterator(i)){
                    int w = e.otherV(i);
                    // 对于每一个边首先判断e->v()可达
                    // 之后看如果e->w()以前没有到达过, 显然我们可以更新distTo[e->w()]
                    // 或者e->w()以前虽然到达过, 但是通过这个e我们可以获得一个更短的距离, 即可以进行一次松弛操作, 我们也可以更新distTo[e->w()]
                    if(from[e.v()] != null && (from[e.w()] == null || distTo[e.v()].doubleValue() + e.weight().doubleValue()<distTo[e.w()].doubleValue())){
                        distTo[e.w()] = distTo[i].doubleValue()+e.weight().doubleValue();
                        from[e.w()] = e;
                    }
                }
            }
        }

        hasNegativeCycle = hasNegativeCycle();
    }

    // 判断图中是否有负权环
    private boolean hasNegativeCycle(){
        for(int i=0; i<graph.V(); i++){
            for(Edge<Weight> e : graph.adjIterator(i)){
                if(from[e.v()]!=null && (from[e.w()] == null || distTo[e.v()].doubleValue() + e.weight().doubleValue()< distTo[e.w()].doubleValue())){
                    return true;
                }
            }
        }

        return false;
    }

    // 返回图中是否有负权环
    public boolean negativeCycle(){
        return this.hasNegativeCycle;
    }

    // 返回从s点到w点的最短路径长度
    public Number shortestPathTo(int w){
        assert w >= 0 && w < graph.V();
        assert !hasNegativeCycle;
        assert hasPathTo(w);
        return distTo[w];
    }

    // 判断从s点到w点是否联通
    public boolean hasPathTo( int w ){
        assert( w >= 0 && w < graph.V());
        return from[w] != null;
    }

    // 寻找从s到w的最短路径, 将整个路径经过的边存放在vec中
    private Vector<Edge<Weight>> shortestPath(int w){
        assert w >= 0 && w < graph.V();
        assert !hasNegativeCycle;
        assert hasPathTo(w);

        // 通过from数组逆向查找到从s到w的路径, 存放到栈中
        Stack<Edge<Weight>> stack = new Stack<>();
        Edge<Weight> e = from[w];
        while (e.v()!= this.s){
            stack.push(e);
            e = from[e.v()];
        }
        stack.push(e);

        // 从栈中依次取出元素, 获得顺序的从s到w的路径
        Vector<Edge<Weight>> vector = new Vector<>();
        while (!stack.isEmpty()){
            vector.add(stack.pop());
        }

        return vector;
    }

    // 打印出从s点到w点的路径
    public void showPath(int w){
        assert( w >= 0 && w < graph.V() );
        assert( !hasNegativeCycle );
        assert( hasPathTo(w) );

        Vector<Edge<Weight>> vector = shortestPath(w);
        for (int i=0; i<vector.size(); i++){
            System.out.print(vector.elementAt(i).v()+"->");
            if( i == vector.size()-1 ){
                System.out.println(vector.elementAt(i).w());
            }
        }
    }
}

四. 单源最短路径算法

Dijkstra 无负权边 有向无向图均可 O(ElogV)
Bellman-Ford 无负权环 有向图 O(EV)
利用拓扑排序 有向无环图(DAG) 有向图 O(E+V)

五. 所有对最短路径算法

  • 应用V次单源最短路径算法
  • Floyed算法,使用了动态规划的算法范式,处理无负权环的图,时间复杂度为O(V^3).

六. 最长路径算法

  • 最长路径问题不能有正权环
  • 无权图的最长路径问题是指数级难度的
  • 对于有权图,不能使用Dijkstra求最长路径
  • 可以使用Bellman-Ford算法求最长路径(权值加负)

猜你喜欢

转载自blog.csdn.net/jt102605/article/details/84727198
今日推荐