算法学习——图之最短路径

前言

之前的两篇文章中,我主要探讨了无权图的路径问题和有权图的最小生成树。但是很多时候,我们往往更会关注两个点之间的最短路径是怎么样的?在无权图中,我们已经通过广度优先遍历找到了最短路径。但是在有权图中,最短路径其实是消耗最少的路径,因此权值在路径的查找中起着至关重要的作用。
最短路径问题

Dijkstra最短路径

原理

Dijkstra也是作者的名字,在平时的应用中,Dijkstra算法也是很普遍的,本人就曾在无线传感网课程论文中见到用这个算法对数据进行传输的。
Dijkstra的使用要求是:不能有负权边,这个跟之后的实现有关。
dijkstra
这个实现就是松弛操作。我们每次先查找初始节点所有邻居节点的距离,然后找到当前节点中路径最短的那个节点2;通过上图的2号节点,我们进行松弛操作,绕道2,我们可以到达其他节点,通过比较找到一条更短的路径0-2-1取代原来的0-1。
这样我们每次都保留最短的,就能得到最后的最短路径树。这也是我们不能有负权边的原因,因为这个算法就决定了我们是不能查找已经确定的顶点的,如果有负权边的话往往还需要我们进行回溯,这在Dijkstra算法中是行不通的。
最短路径树

代码实现

首先它也需要一些参数,解释在各个参数边上的注释中:

private:
    Graph &G;//图的引用
    int s;//源节点

    Weight *distTo;//distTo[i]存储从起始点s到i的最短路径长度
    bool *marked;//节点是否被访问
    vector< Edge* > from;//from[i]记录最短路径中, 到达i点的边是哪一条

构造函数我们使用了最小索引堆这个数据结构。

  1. 首先初始化,将源节点s加入最小索引堆中
  2. 然后弹出堆中最小的(初始时是s),遍历s的各个邻边
  3. 对每个邻边另一个节点进行松弛操作,然后压入堆中
  4. 重复操作2,直到所有的节点都遍历完毕
public:
    Dijkstra(Graph &graph, int s):G(graph){

        assert( s >= 0 && s < G.V() );

        //初始化
        this->s = s;
        distTo = new Weight[G.V()];
        marked = new bool[G.V()];
        for(int i = 0; i < G.V(); i++){
            distTo[i] = Weight();
            marked[i] = false;
            from.push_back(NULL);
        }

        IndexMinHeap ipq(G.V());

        //Dijkstra
        //初始化
        distTo[s] = Weight();//默认构造函数为0
        marked[s] = true;//访问s
        from[s] = new Edge(s, s, Weight());

        ipq.insert( s, distTo[s] );//压入最小索引堆

        while( !ipq.isEmpty() ){
            int v = ipq.extractMinIndex();

            //distTo[v]就是s到v的最短距离
            marked[v] = true;

            typename Graph::adjIterator adj(G, v);
            for( Edge* e = adj.begin(); !adj.end(); e = adj.next() ){
                int w = e->other(v);
                if( !marked[w] ){
                    if( from[w] == NULL || distTo[v] + e->wt() < distTo[w] ){
                        distTo[w] = distTo[v] + e->wt();
                        from[w] = e;
                        //维护最小索引堆
                        if(ipq.contain(w))
                            ipq.change(w, distTo[w]);
                        else
                            ipq.insert(w, distTo[w]);
                    }
                }
            }
        }
    }

Bellman-Ford算法

原理

Dijkstra算法是一个可以高效找到最短路径的算法,算法复杂度为O(ElogV),但是它也有限制,就是不能存在负权边。因此,Bellman-Ford单源最短路径算法就被发明出来,解决了这个问题。
我们同样还是使用刚刚的松弛操作,如果一个节点,对所有除它自己以外的邻接节点进行松弛操作,这样最多进行V-1次操作,就可以得到我们想找到的最短路径了。
负权边
不过在查找最短路径的时候,我们还需要进行负权环的判断。因为如果一张图中有了负权环(一个环权值之和为负),那么它是不存在最短路径的。判断也相对比较简单,因为如果有最短路径,最多进行V-1次松弛操作,如果超过了V次说明存在负权环,那么这张图也就不存在最短路径了。
负权环
同时这个算法在效率上是有一定的牺牲的,因为要进行V轮遍历边操作,所有最后的时间复杂度为O(EV)。

代码实现

我们的参数与之前不同的是添加了一个判断负权环的操作:

private:
    Graph &G;//图的引用
    int s;//源
    Weight* distTo;// distTo[i]存储从起始点s到i的最短路径长度
    vector< Edge* > from;//from[i]记录最短路径中, 到达i点的边是哪一条
    bool hasNegativeCycle;//是否有负权环

    bool detectNegativeCycle(){
        //进行V-1次操作后仍然有更小的,说明有负权环
        for(int i = 0; i < G.V(); i++){
            typename Graph::adjIterator adj(G, i);
            for( Edge* e = adj.begin(); !adj.end(); e = adj.next() )
                if(from[e->v()] && distTo[e->v()] + e->wt() < distTo[e->w()])
                    return true;
        }

        return false;
    }

我们进行V-1轮循环,每次循环都能得出经过pass步得到的最短路径,因为不存在负权环,所以当我们进行了V-1轮松弛操作之后,所有节点的最短路径就得出了。
证明:

如果一个图没有负权环,
从一个节点到另一个节点的最短路径,最多经过所有V个顶点,有V-1条边
如果有个顶点经过了两次,说明存在环,这个环一定是负权环

public:
    BellmanFord(Graph &graph, int s):G(graph){
        this->s = s;
        distTo = new Weight[G.V()];
        for(int i = 0; i < G.V(); i++){
            from.push_back(NULL);
        }

        //Bellman-Ford
        distTo[s] = Weight();//初始化为0
        from[s] = new Edge(s, s, Weight()); // 这里我们from[s]的内容是new出来的, 注意要在析构函数里delete掉

        // 进行V-1次循环, 每一次循环求出从起点到其余所有点, 最多使用pass步可到达的最短距离
        for(int pass = 1; pass < G.V(); pass++){

            // 每次循环中对所有的边进行一遍松弛操作Relaxation
            for(int i = 0; i < G.V(); i++){
                typename Graph::adjIterator adj(G, i);
                for(Edge* e = adj.begin(); !adj.end(); e = adj.next())
                    if( from[e->v()] && (!from[e->w()] || distTo[e->v()] + e->wt() < distTo[e->w()]) ){
                        distTo[e->w()] = distTo[e->v()] + e->wt();
                        from[e->w()] = e;
                    }
            }
        }

        hasNegativeCycle = detectNegativeCycle();
    }

图片引用百度图片
代码实现参照liuyubobobo慕课网教程

你可能感兴趣的:(算法)