图算法(二)最短路

本文将介绍三种常见的最短路算法:Dijkstra,Floyd,SPFA

Dijkstra

Dijkstra是有向图上的单源最短路径算法,本质是一种贪心。给定一个有向图G(V,E)和起点s,基础的Dijkstra算法会在O(|V|^2)的时间复杂度内求出从s出发到所有点的最短路长度。

Dijkstra算法要求图中不能有负权边,其算法描述如下:

  1. 建立一个空的优先队列Q;
  2. 把所有顶点根据与s的距离dis[i]插入优先队列,其中dis[s]=0,与s不相连的顶点距离设为INF;
  3. 每次从Q中取出与s距离最近的顶点u,对点u的每条出边e=(u,v),更新dis[v] = min(dis[v],dis[u] + l(u, v));
  4. 重复3操作,直到所有顶点都被取出,结果中dis[i]代表s到i的最短距离。

如果用数组维护dis[i],那么每次查找与s最近的点和更新操作代价都是O(|V|),算法整体复杂度O(|V|^2),这个复杂度在稠密图的情况下是很理想的;如果用堆(优先队列)实现,那么每次查找最近点的代价变成O(lg|V|),在堆中的decrease-key操作是O(lg|V|)的,算法中最多有|V|次查找和|E|次更新,算法整体复杂度O((|V| + |E|) lg|V|),在稀疏图中是一种优化。

正确性证明:
Dijkstra每次取出的点都有唯一的“前驱”,因此我们是可以恢复出一条从起点到终点的路径的,我们只要证明这条路径就是达到目标点的最短路径。采取归纳法证明:

  1. 除起点s外第一个被取出的点u找不出比s->u更短的路径。假如有一条更短的路:s->v...->u,由于不存在负权边,那么l(s,v)<l(s,u),这与Dijkstra每次取dis最短的点矛盾;
  2. 假设已经被取出的点都满足条件,Dijkstra选中的下一个与s最近的点是v,其前驱是u(u可能与s重合),那么Dijkstra给出的路径L1为:s->s1->...->sk->u->v,其中s1...sk,u都是已经被取出的点,其中dis[u]+l(u,v)是最小的。假设有一条从s到v的路径L2长度小于L1,那么L2中v的前驱不能是u(如果是,与归纳假设矛盾),如果不是u,则只能是一个还没被取出的点t(如果不是,与刚才dis[u]+l(u,v)的最小性矛盾),那么此时dis[t]<dis[v],v不应该被从队列中选取,矛盾!
dijkstra.jpg

C++实现:

void dijkstra(int s) {  // s: starting vertix
    std::priority_queue<Node, std::vector<Node>, std::greater<Node> > q;
    for(int i = 0; i < n; i++)
        dis[i] = INF;
    dis[s] = 0;
    q.push(std::make_pair(0, s));

    for(int i = 0; i < n; i++) {
        while(!q.empty() && q.top().first > d[q.top().second])
            q.pop();
        // the graph may be unconnected
        if(q.empty()) break;
          
        int hd = q.top().second;
        q.pop();

        for(Edge *p = e[hd]; p; p = p->next)
            if(dis[hd] + p->len < dis[p->dst])
                q.push(std::make_pair(dis[p->dst] = p->len + dis[hd], p->dst));
    }
}

Floyd

Floyd是有向图上的多源最短路算法,本质是动态规划。给定有向图G(V, E),Floyd算法可以在O(|V|^3)的时间复杂度内算出图上任意两点之间的最短距离。算法描述如下:

  1. 初始化邻接矩阵dis:i=j时,dis[i][j] = 0,i != j时,若有从i到j的有向边e,dis[i][j]等于e的权值,若没有,dis[i][j] = INF;
  2. 选取一个新的中间节点k,对于所有的(i, j)对,如果dis[i][j] < dis[i][k] + dis[k][j],则更新dis[i][j]的值为dis[i][k] + dis[k][j];
  3. 重复操作2,直到所有的点都成为过中间节点,结果中dis[i][j]表示i到j的最短距离。

正确性证明:
Floyd算法中的中间节点k可以理解为:从起点i到终点j,只经过1-k这些点可以得到的最短路是多少。算法结束时dis[i][j]对应的就是从顶点i到顶点j只经过顶点1-|V|的最短路,就是所求的结果。所以我们只要证明:前k个中间节点处理完后,dis[i][j]的值为从起点i只经过1-k中的点到达终点j的最短长度。用归纳法描述:

  1. k=1时显然(dis[i][j]要么是i和j的距离,要么是i到1的距离加上1到j的距离,且保证是两者中较短的)
  2. 如果k-1得证,对于k的情况,从i到j的最短路要么经过k,要么不经过k。经过k时,dis[i][j]会更新为dis[i][k]+dis[k][j],否则dis[i][j]不变。这两种情况途径点的标号都不会超过k,而且由归纳假设可确保是最短的。

C++实现:

void floyd() {
    // suppose dis matrix is intialized
    for(int k = 1; k <= n; k++)
        for(int i = 1; i <= n; i++)
            for(int j = 1; j <= n; j++)
                dis[i][j] = std::min(dis[i][j], dis[i][k] + dis[j][k]);
}

因为只是一个三重循环,Floyd算法一定会终止,那么它是否能处理有负边的情况呢?答案是可以的,负边对我们的归纳并没有影响。

SPFA

SPFA(Shortest Path Faster Algorithm)是有向图上的单源最短路算法,它最大的亮点是可以处理负边,而且在大部分情况下运行效率很高。其算法描述如下:

  1. 初始化dis数组,其中dis[s]为0,其他值为INF,初始化一个队列,其中只有s一个元素;
  2. 从队列中取出第一个元素hd,对hd的每一条出边e(hd, i),更新dis[i] = min(dis[i], dis[hd] + weight[hd][i]),如果dis[i]被更新(更新有时被称为松弛操作)了且i不在队列中,那么把i加入队列末尾;
  3. 重复2操作直到队列为空

正确性证明:
SPFA的正确性是三种算法中最不显然的。首先证明算法是会终止的,向队列中加入顶点i要求dis[i]被更新为更小的值,而没有负环时,dis[i]是有下界的,即每个点只会被放入队列有限次,每一次循环都会取出一个顶点,所以没有负环时,循环一定会终止。而在有负环的时候,SPFA会陷入死循环。
接着证明SPFA所得的dis[i]就是从起点s到终点i的最短路,先证明一个引理:每次检查队列是否为空时,所有能引起松弛操作的点都在队列中。

We want to show that if dis[w] > dis[u] + weight[u][w] at the time the condition is checked, u is in the queue. We do so by induction on the number of iterations of the loop that have already occurred. First we note that this certainly holds before the loop is entered: if u!=v, then relaxation is not possible; relaxation is possible from u=v, and this is added to the queue immediately before the while loop is entered. Now, consider what happens inside the loop. A vertex u is popped, and is used to relax all its neighbors, if possible. Therefore, immediately after that iteration of the loop, u is not capable of causing any more relaxations (and does not have to be in the queue anymore). However, the relaxation by u might cause some other vertices to become capable of causing relaxation. If there exists some x such that dis[x] > dis[w] + weight[w][x] before the current loop iteration, then w is already in the queue. If this condition becomes true during the current loop iteration, then either dis[x] increased, which is impossible, or dis[w] decreased, implying that w was relaxed. But after w is relaxed, it is added to the queue if it is not already present.

SPFA算法结束时队列为空,代表没有松弛操作可以做了。而我们给出一个dis数组,它是最短路问题解的充要条件就是不可以再执行松弛操作。所以SPFA给出的解一定正确的。

时间复杂度:
段凡丁在提出SPFA的论文中指出SPFA的时间复杂度是O(|E|)的,但原文的证明很不严谨,关键在于他的一个结论:平均每个点进入队列的次数是一个常数。我暂时也没有找到很好的证明,不过一般认为SPFA的平均时间复杂度就是O(|E|)的。值得一说的是,SPFA在效率上并没有Dijkstra稳定,原因就在于顶点可能多次被加入队列,如果很多点都存在“多跳路径短于少跳路径”的话,SPFA就会变得很慢。

C++实现:

void spfa(int s) {
    std::queue<int> q;
    for(int i = 0; i < n; i++)
        dis[i] = INF;
    dis[s] = 0;
    q.push(s); vis[s] = true;

    while(!q.empty()) {
        int hd = q.front();
        q.pop(); vis[hd] = false;
        for(Edge *p = e[hd]; p; p = p->next)
            if(dis[hd] + p->len < dis[p->dst]) {
                dis[p->dst] = dis[hd] + p->len;
                if(!vis[p->dst])
                    q.push(p->dst), vis[p->dst] = true;
            }
    }
}

本文图片来自 Lecture Slides for Alogorithm Design by Jon Kleinberg and Éva Tardos.

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,378评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,356评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,702评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,259评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,263评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,036评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,349评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,979评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,469评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,938评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,059评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,703评论 4 323
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,257评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,262评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,485评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,501评论 2 354
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,792评论 2 345

推荐阅读更多精彩内容