【经典算法】Dijkstra单源最短路径算法

单源最短路径问题

给定一个带权有向图 G=(V,E) ,其中每条边的权是一个非负实数。另外,还给定 V 中的一个顶点,称为源。现在我们要计算从源到所有其他各顶点的最短路径长度。这里的长度是指路上各边权之和。这个问题通常称为单源最短路径问题。

前面Bellman-Ford最短路径算法讲了单源最短路径的Bellman-Ford算法(动态规划算法)。这里介绍另外一个更常见的算法Dijkstra算法。

Dijkstra算法和 最小生成树Prim算法最小生成树算法非常类似,大家可以先熟悉下个算法。两个算法都是基于贪心算法。虽然Dijkstra算法相对来说比Bellman-Ford 算法更快,但是不适用于有负权值边的图,贪心算法决定了它的目光短浅。而Bellman-Ford 算法从全局考虑,可以检测到有负权值的回路。

这里模仿MST(Minimum Spanning Tree)的Prim算法,我们创建一个SPT(最短路径树),最初只包含源点。我们维护两个集合,一组已经包含在SPT(最短路径树)中的顶点S集合,另一组是未包含在SPT内的顶点T集合。每次从T集合中选择到S集合路径最短的那个点,并加入到集合S中,并把这个点从集合T删除。直到T集合为空为止。

T集合用最小优先队列Q存储,该队列包含所有属于V-S的节点(这些节点尚未确定最短路径的权),且以d值为关键字排列各节点。

初始时,Q包含了除源点src以外的其他节点,这些节点的d值为无穷大。源点src进入S之后,d[src]=0。算法反复从Q中取出d值最小的节点u,u属于V-S,把u插入集合S中,并对u的所有出边进行松弛,这一过程直到Q队列为空为止。

举例

如下图所示的图:

《【经典算法】Dijkstra单源最短路径算法》

S集合最初为空,然后选取源点0,S集合为 {0},源点到其它所有点的距离为 {0, INF, INF, INF, INF, INF, INF, INF} 。图中蓝色表示 SPT,迭代的过程如下:

《【经典算法】Dijkstra单源最短路径算法》

《【经典算法】Dijkstra单源最短路径算法》

《【经典算法】Dijkstra单源最短路径算法》

《【经典算法】Dijkstra单源最短路径算法》

最终得到 SPT(最短路径树) 如下:

《【经典算法】Dijkstra单源最短路径算法》

注意点

  • The code calculates shortest distance, but doesn’t calculate the path information. We can create a parent array, update the parent array when distance is updated (like prim’s implementation) and use it show the shortest path from source to different vertices.

  • The code is for undirected graph, same dijekstra function can be used for directed graphs also.

  • The code finds shortest distances from source to all vertices. If we are interested only in shortest distance from source to a single target, we can break the for loop when the picked minimum distance vertex is equal to target (Step 3.a of algorithm).

  • Time Complexity of the implementation is O(V^2). If the input graph is represented using adjacency list, it can be reduced to O(E log V) with the help of binary heap. We will soon be discussing O(E Log V) algorithm as a separate post.

  • Dijkstra’s algorithm doesn’t work for graphs with negative weight edges. For graphs with negative weight edges, Bellman–Ford algorithm can be used, we will soon be discussing it as a separate post.

时间复杂度分析

算法的执行速度取决于优先队列Q的数据结构。有三种数据结构可供选择。

  • 用一维数组实现优先队列Q。在该算法中,每次都是从最小优先队列Q中取出d值最小的节点需要的时间为O(V),存在|V|次这样的操作,所以最小优先队列Q取出d值最小的节点的全部运行时间为O(V^2)。因为每个节点v属于V仅被插入集合S一次,所以在算法的执行过程中,v的每条邻接边在for循环中仅被考察一次。因为在所有邻接边的总数为|E|,所以在该for循环中总共存在|E|次迭代,每次迭代运行时间为O(1),导致整个算法的运行时间为O(V^2+E)=o(V^2) 。显然对于规模不大的稠密图,可次啊用数组实现优先队列。
  • 用二叉堆实现优先队列Q。在该算法中,每次从最小优先队列Q取出d值最小的节点需要的时间为O(lnV),存在|V|次这样的操作。建立二叉堆需要的时间为O(V)。在relax过程中的赋值语句d[v] <- d[u]+w(u,v)是通过调整节点v在二叉堆的位置来完成的,运行时间为O(lnV),并且至多存在|E|次这样的操作。因此算法的时间复杂度O((V+E)lnV)。通常情况下,边数|E|都不小于节点数|V|,所以运行时间可以简化为O(ElnV)。显然在稀疏图的情形下用二叉堆来实现优先队列是比较实用。
  • 用Fibonacci堆实现优先队列。运行时间为O(VlnV+E)。

代码

/*------------------------------------- * 日期:2015-04-23 * 作者:SJF0115 * 题目: Dijkstra算法(单源最短路径) * 博客: ------------------------------------*/
#include <iostream>
#include <climits>
#include <vector>
using namespace std;


//从未包含在SPT的集合T中,选取一个到S集合的最短距离的顶点
int GetMinVertex(int dist[], bool visited[],int v) {
    int min = INT_MAX;
    int index;
    for(int i = 0;i < v;++i){
        // 没访问过且距SPT最短的顶点
        if(!visited[i] && dist[i] < min){
            min = dist[i];
            index = i;
        }//if
    }//for
    return index;
}

// 打印结果
void Print(int dist[],int n){
    for(int i = 0;i < n;++i){
        cout<<"距离顶点"<<i<<"最短距离->"<<dist[i]<<endl;
    }//for
}

//source 代表源点
void Dijkstra(vector<vector<int> > graph,int src) {
    // 顶点个数
    int v = graph.size();
    // dist[i]表示从源点到顶点i的距离
    int dist[v];
    // visited[i]=true 如果顶点i包含在SPT中
    bool visited[v];
    // 初始化 0代表不可达
    for(int i = 0;i < v;++i){
        dist[i] = (graph[src][i] == 0 ? INT_MAX:graph[src][i]);
        visited[i] = false;
    }//for
    dist[src] = 0;
    visited[src] = true;

    // 迭代V-1次,因此不用计算源点了,还剩下V-1个需要计算的顶点。
    for(int i = 1;i < v;++i){
        // T集合中到S集合距离最短的顶点
        int u = GetMinVertex(dist,visited,v);
        // 加入SPT中
        visited[u] = true;
        // 更新T集合中顶点到S集合顶点的距离
        for (int j = 0;j < v;++j){
            if (!visited[j] && graph[u][j] && dist[u] != INT_MAX
                    && dist[u] + graph[u][j] < dist[j]){
                dist[j] = dist[u] + graph[u][j];
            }//if
        }//for
    }//for
    // 打印结果
    Print(dist,v);
}

int main() {
    vector<vector<int> > graph =
        {
            {0, 4, 0, 0, 0, 0, 0, 8, 0 },
            {4, 0, 8, 0, 0, 0, 0, 11, 0 },
            {0, 8, 0, 7, 0, 4, 0, 0, 2 },
            { 0, 0, 7, 0, 9, 14, 0, 0, 0 },
            { 0, 0, 0, 9, 0, 10, 0, 0, 0 },
            { 0, 0, 4, 0, 10, 0, 2, 0, 0 },
            { 0, 0, 0, 14, 0, 2, 0, 1, 6 },
            { 8, 11, 0, 0, 0, 0, 1, 0, 7 },
            { 0, 0, 2, 0, 0, 0, 6, 7, 0 }
        };
    // 单源最短路径
    Dijkstra(graph,0);
    return 0;
}

点赞