图论算法之最短路径

图论算法之最短路径



作者:jasonkent27

转载请注明出处:www.cnblogs.com/jasonkent27

1. 前言

1.1 最短路引入

小明和小天现在住在海口(C1),他们俩计划暑假到三亚(C4)玩一趟,在海口和三亚之间有许多中间城市(文昌,临高,乐东,万宁...)图中的边上的数字是他们到达该城市必须的花费,现在需要你帮他们俩找出一条从海口到三亚的最省钱的路径出来。



等等,图中的边的weight怎么会有负的呢?你暂且可以这么理解吧。图中的边上的weight可以当作他们旅途中必须的花费,但是他们俩在去三亚图中把钱花光了(真是败家)不得不通过搬砖赚钱,通过搬砖赚的钱不仅抵掉他们应该的花费,而且还赚了点小钱(但是他们不能一直搬砖啊,不然他们早就累挂了,哪里还能去玩)。

1.2 松弛技术

对于每个节点v,我们用dist[v] 来表示原点s到v的估算距离,用delta(s,v)表示s到v的最短距离

对边进行松弛操作的过程如下:

void relax(int u, int v, int w)
{
  if ( dist[v]> dist[u] + w )
      dist[v] = dist[u] + w ;
}

1.3 初始化

   void init(int v0)
   {
       for (int i = 1 ; i <= n ; i ++)
           dist[i] = infinity ;
       dist[v0] = 0 ;
   }

2. 最短路径的几个性质

  1. 最短路径具有最优子结构性质
  2. 路径松弛性质
  3. 上界性质
  4. 收敛性质
  5. 最短路径一定是简单路径

这几条性质是后面最短路算法的数学依据,如果想要彻底理解最短路的算法,有必要对其深究

2.1 最短路径的最优子结构性质

即最短路径的子路径也是最短路径

简单证明:如果最短路径P的子路径不是最短路径,则可以找到一条更短的路径使得P更短,这与P是最短路径不符.

2.2 路径松弛性质

如果P=<v0,v1, ..., vk>是从源点s=v0到vk的一条最短路径,并且我们对P中的边所进行松弛的次序为(v0,v1),(v1,v2), ..., (vk-1,vk),则dist[vk] = delta(s,vk),why?

我们可以用归纳法进行证明:

1.当v = s,时 dist[v] = delta(s,s) = 0,显然成立

2.假设当v = vk-1时,dist[v] = delta(s,v) = delta(s,vk-1)成立,如果我们能推出dist[vk] = delta(s,vk),则命题成立。

因为

dist[vk] >= dist[vk-1] + w(vk-1,vk)

= delta(s,vk-1) + w(vk-1,vk)

= delta(s,vk) (收敛性质)

则原命题也是成立的.

2.3 上界性质

对于所有节点v, 有dist[v] >= delta(s,v),且当dist[v]=delta(s,v)之后,dist[v]的值不会再变化.

此性质亦可用归纳法证明:

1.进行init(v0)后,显然满足对所有v, dist[v] >= delta(s,v)

2.假设松弛前,对所有v, dist[v] >= delta(s,v),我们只要再证明松弛后,

2.4 收敛性质

假设s-->u-->v为s-->v的一条最短路径,在某个时刻dist[u] = delta(s,u),则在边(u,v)松弛之后有dist[v] = delta(s,v)

证明:在边松弛之后应该有

dist[v] <= dist[u] + w(u,v)

= delta(s,u) + w(u,v)

= delta(s,v) (最短路最优子结构性质)

而根据上界性质,dist[v] >= delta(s,v),因此dist[v] = delta(s,v)

2.4 最短路径一定是简单路径

why? 为何最短路径一定是简单路径(即无环)?

理由如下:假设最短路径有环,则有以下三种情况:

1.正环,然后去掉正环会使最短路径更短,不符合

2.零环,零环对最短路径没有影响,可以去掉

3.负环,如果存在负环,则不会有最短路径,因为我循环一圈都可以是u到v的路径更短.

3. Bellman-Ford算法

3.1 算法思路

Bellman-Ford算法通过不断构建以源点为根的最短路径树,不断扩展.

简单来说,先找出,扩展一条边,源点到达其他节点的最短路径,再找出扩展两条边,源点到达其他节点的最短路径,以此类推,找到通过扩展n-1条边,源点到达其他节点的最短路径,算法完毕。因为我们知道最短路径是简单路径,因此它无环,所以它最多有n-1条边.算法思路类似bfs.其正确性可以用路径松弛性质证明.

示意图如下(图片来自算法导论):

先是从s扩展一条边,可以达到t,y(图b),此时我们知道,在只扩展一条边的前提下,s到t,y的最短路径分别是dist[t],dist[y],然后再通过t,y继续扩展边(图c),这时我们有,在只扩展两条边的前提下,s到x,z的最短路径分别是dist[x],dist[z]。当然有个前提假设是s到其他节点都有最短路径.看看整个过程有没有觉得特别像通过bfs找最短路的过程.

3.2 算法伪代码

    bool Bellman_Ford(int v0)
    {
        init(v0) ;
        for(int i=1 ; i<=n-1 ; i++)
            for(int j=1 ; j<=m ; j++)
            relax(edge[j].u,edge[j].v,edge[j].w);
        for(int i=1 ; i<=m ; i++)
        {
            int u = edge[i].u ;
            int v = edge[i].v ;
            int w = edge[i].w ;
            if (dist[v] > dist[u] + w)
                return false ;
        }
        return false ;
    }

4. SPFA算法

4.1 算法思想

我们可以轻松知道Bellman-Ford算法的时间复杂度是O(nm)的,我们再想想,是不是每次都需要对M条边进行松弛操作的,显然没必要,而且我们发现如果在某次循环中,发现对M条不管怎么relax,dist数组都不会变化,那么算法就可以停止了(上界性质).SPFA算法就是在这2点上对Bellman-Ford算法进行优化的。SPFA算法时间复杂度为O(qm),其中q远远小于n.在实战中有不错的效率,不过可以造出让SPFA效率低的数据。因此在如果题目没有负边,个人倾向于基于优先队列实现的Dijkstra算法.

4.2 算法伪代码

    void SPFA(int v0)
    {
        queue<int> q ;
        init(v0) ;
        q.push(v0) ;
        inq[v0] = true ;
        while (!q.empty())
        {
            int u = q.front(); q.pop();
            inq[u] = false ;
            for (int i=1 ; i<=n ; i++) //此处若用邻接表则更快
            if (g[u][i] !=0 && dist[i] > dist[u]+g[u][i])
            {
                dist[i] = dist[u]+g[u][i] ;
                if (inq[i] == false)
                {
                    q.push(i);
                    inq[i] = true ;
                }
            }
        }
    }

5. Dijkstra算法

5.1 算法思路

维护集合U,用来保存那些已经计算过最短路的节点即dist[v] = delta(s,v)的节点,不断增大集合U,直到U包含图中所有节点.

1.选出V-U集合中最小的dist[x]

2.把x加入到U中

3.用dist[x]去更新所有V-U集合的所有节点dist值,(U集合中的dist值已经达到下限delta(s,v)了,没必要再更新)

5.2 算法伪代码

    void Dijkstra(int v0)
    {
        init(v0) ;

        for (int i=1 ; i<=n-1 ; i++)
        {
            min = infinity ;
            for (int j=1 ; j<=n ; j++)
                if (!inq[j] && dist[j]<min)
                {
                    min = dist[j] ;
                    k = j ;
                }
            inq[k] = true ;
            for(int j = 1 ; j<=n ; j++)
                if (!inq[j] && dist[j]>dist[k]+g[k][j])
                    dist[j]>dist[k]+g[k][j] ;
        }
    }

5.3 算法理解

算法执行过程

1.首先从V-U集合中选出dist值最小的节点s(图a),然后拿它去更新其他节点(图b)

2.把节点s加入到U集合中

3.不断重复此过程,直到U=V位置(图c,d,e,f)

要证明算法的正确性,我们只需要证明当节点u加入到集合U时,有dist[u]=delta(s,u)即可.

用反证法:假设u是第一个加入到集合U时,满足dist[u] != delta(s,u)的节点.

由于节点s是第一个加入到集合U的,并且有dist[s] = delta(s,s)=0,因此节点u与节点s必然不同,因此一定存在某条从节点s到节点u的路径,否则dist[u] = infinity,是不会被选入集合U中的.因为至少存在一条从s到u的路径,因此也存在一条从s到u的最短路径.

考虑这样一条路径s---->x-->y---->u,其中,s,x在集合U中,y,u在V-U集合中,

  • 因为x在u加入之前就已经加入到U集合中了,因此有dist(x) = delta(s,x),而x是y前驱,对边(x,y) relax后,有dist[y] = > delta(s,y)(收敛性质)
  • 因为y是u的前驱,且边权值非负,因此有dist[y] = delta(s,y) <=delta(s,u)<=dist[u].
  • 另外,算法选择u节点放进集合U的前提是u是所有V-U集合中dist值最小的,因此有dist[y] >= dist[u].

    综上,有dist[y] = dist[u] = delta(s,u).这与假设dist[u] != delta(s,u)不符.因此原命题是成立的.

6. floyd算法

6.1 算法引入

floyd算法用来求图中任意点对之间的最短路径.将floyd算法之前我们先来看看另外一个用O(V^4)来解决图中任意点对之间最短路径的算法。

通过前面的知识我们知道,任意两个节点之间的最短路径无非就两种情况

1.i, j 直接相连

2.i, j 间接相连

若是直接相连,则delta(i,j) = g[i][j] ;

若间接相连,设其路径为i------>k-->j(k为j前驱),则有

delta(i,j) = delta(i,k) + g[k][j] (收敛性质)

我们定义dist[m][i][j] 表示最多可以通过m条边,节点i,j之间的最短距离,则有dist[m][i][j] = min(dist[m-1][i][j],dist[m-1][i][k] + g[k][j]) = min(dist[m-1][i][k] + g[k][j]),其中1<=k<=V,dist[0][i][j] = g[i][j].

伪代码如下

    void Shortest_Path_Pair()
    {
        for (int u = 1 ; u<=n-1 ; u++) //因为最短路最多只有n-1条边
        for (int i=1 ; i<=n ; i++)
        for (int j = 1 ; j<=n ;j++ )
            for (int k=1 ; k<=n ; k++)
            //当然此处dist[u][i][j]可以降维成dist[i][j].
            dist[u][i][j] = min(dist[u-1][i][j],dist[u-1][i][k]+g[k][j]) ;
    }

6.2 算法思路

OK,热身结束!我们来看看floyd算法,上面算法类似于Bellman_Ford,他们都是从路径中边的性质来考虑的。现在我们换个思路,从中间节点开始考虑,考虑节点i,j间最短路径p与中间节点均取自{1,2,...,k-1}的关系.

  • 如果节点k不是路径p上的中间节点,则节点i,j路径上的中间节点均取自{1,2,...,k-1}的最短路径也是节点i,j路径上的中间节点均取自{1,2,...,k}的最短路径
  • 如果节点k是路径p上的中间节点,则我们可以把路径分解成i--->k--->j,对于路径i-->k,其路径上的节点也肯定全部来自{1,2,...,k-1},因为k不是路径i-->k的中间节点,路径k-->j同理,看下图:

因此我们可以定义d[k][i][j] 表示i,j之间节点编号最大为k时的最短距离,我们有

d[k][i][j] = min(d[k-1][i][j], d[k-1][i][k]+d[k-1][k][j])

当然d[k][i][j]也是可以降维成d[i][j].其中1<=k<=V,d[0][i][j] = g[i][j].

6.3 伪代码

    void Floyd()
    {
        for (int k = 1 ; i<=n ; k++)
            for (int i = 1; i<=n ; i++)
            for (int j = 1 ; j<=n ; j++)
                if (d[i][j] > d[j][k] + d[k][j])
                    d[i][j] = d[j][k] + d[k][j] ;
    }

后记

本文对求最短路径的几种基本算法做了简单的讲解,力求做到简单,易懂,更严格的算法正确性的证明可以查阅参考书目,文中有任何错误之处请指出,我会尽快改正.另外有何问题,欢迎留言讨论.

参考书目

《算法导论》 第三版

时间: 2024-10-22 04:13:11

图论算法之最短路径的相关文章

基础图论算法导引

ACM中常用图论算法 1. 拓扑排序 -> 拓扑排序的原理及其实现 2. 最短路径算法 -> 最短路算法总结 差分约束系统 -> 差分约束 前k短路 -> 前K短路径问题 3. 最小生成树问题扩展 -> 最?小?生?成?树?问?题?的?拓?展  最优比率生成树 -> 最优比率生成树 最小k度限制生成树 -> IOI2004国家集训队论文,由汪汀所著(网盘内有) 或者刘汝佳的黑书内有 裸题 poj1639 题解 4. 二分图匹配 -> 二分图的最大匹配.完美匹

再谈排序与图论算法

排序 1.主存能放下的数据进行排序称为内部排序,反之称为外部排序(磁盘上).2.任何进行交换相邻元素进行排序的算法均需要O(N2)的复杂度,任何进行比较的排序算法至少需要O(N*log(N))的算法复杂度. 3.堆排序和归并排序的时间复杂度平均和最坏均为O(N*log(N)) 4.Java中执行一次对象比较是比较昂贵的,移动则是相对节省的,因此归并排序是java的默认泛型排序算法.C++中默认的是快速排序,比较耗费小:快排对于基本类型均具有最快速度.快速排序选取枢纽元的时候采用三数取中,切勿采用

算法系列笔记8(有关图的算法二—最短路径问题)

图的最短路径问题主要分为两类,单源最短路径问题和全对最短路径问题.单源最短路径问题指给点单个源点,求其到所有其它顶点之间的最短距离.而全对最短路径问题指所有顶点之间的最短路劲问题.此外对于单对最短路径问题,从渐进意义上来看,目前还没有比最好的单元算法更快的算法来解决这一问题. 一:单源最短路径问题 单源最短路劲问题根据其权重分为四类,当图G=(V,E)为无权图,直接使用广度优先遍历(这里不做介绍):当权值为非负值,则使用Dijkstra算法:存在负权值及负权环,可以使用Bellman-Ford算

算法_最短路径

一.概述  定义:在一幅加权有向图中,从顶点s到顶点t的最短路径是所有从s到t的路径中的权重的最小者.从定义可以看出单点最短路径的实现是建立在加权有向图的基础上的. 最短路径树:给定一幅加权有向图和一个顶点s,以s为起点的一颗最短路径树是图的一幅子图,它包含s和从s可达的所有顶点.这颗有向树的根节点是s,树的每条路径都是有向图中的一条最短路径.它包含了顶点s到所有可达的顶点的最短路径. 二.加权有向图和加权有向边的数据结构 加权有向图和加权有向边的数据结构和加权无向图无向边的数据结构类型基本相同

图论算法----强连通

poj 2186 Popular Cows 分析:直接求一下强连通分量,对于同一个强连通分量里面的结点状态是相同的,要求有多少个人被其他所有的人都认可,只有可能是拓扑排序的最后一个强连通的结点个数,判断一下其他节点是否都可以到该联通分量就ok了. 1 #include <iostream> 2 #include <cstdio> 3 #include <cstring> 4 #include <set> 5 #include <algorithm>

Bellman-Ford算法(最短路径)

Dijkstra算法是处理单源最短路径的有效算法,但它局限于边的权值非负的情况,若图中出现权值为负的边,Dijkstra算法就会失效,求出的最短路径就可能是错的. 这时候,就需要使用其他的算法来求解最短路径,Bellman-Ford算法就是其中最常用的一个.该算法由美国数学家理查德?贝尔曼(Richard Bellman, 动态规划的提出者)和小莱斯特?福特(Lester Ford)发明. 适用条件&范围: 单源最短路径(从源点s到其它所有顶点v); 有向图&无向图(无向图可以看作(u,v

Dijkstra算法求最短路径(java)(转)

原文链接:Dijkstra算法求最短路径(java) 任务描述:在一个无向图中,获取起始节点到所有其他节点的最短路径描述 Dijkstra(迪杰斯特拉)算法是典型的最短路径路由算法,用于计算一个节点到其他所有节点的最短路径.主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止. Dijkstra一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表方式用OPEN,CLOSE表的方式,其采用的是贪心法的算法策略,大概过程如下:1.声明两个集合,open和close

Dijkstra算法求最短路径

摘自最短路径算法,如有任何侵权问题,请及时通知本人,本人将马上予以删除. 链接算法过程 /* 有向图的构建及最短路径求解(Dijkstra) */ #include <stdio.h> #include <stdlib.h> #define MAX_VERTEX_NUM 30 #define MAX_INT 1000 typedef int VrType; typedef char VtType; bool visted[MAX_VERTEX_NUM]; //搜索时的标记矩阵 ty

图论算法(5) --- 双向广搜求最短路(Bidirectional Breadth First Search)

我们知道,在图论算法中,求最短路是最基本的问题.在求最短路的问题中,应用双向广度优先搜索算法,又是一个较为高效而又简单的算法.所谓双向广度优先搜索,其实根本的核心还是BFS,只不过它是从起点和终点两头同时搜索,大大提高了搜索效率,又节省了搜索空间.广搜大家知道当然是用队列来实现了,在这里,要注意的问题就是,我们必须按层搜索,正向队列处理一层,接着去处理反向队列的一层,按层交替进行,而不是按节点交替进行,这点需要注意,其他的也就很简单了,代码中附有注释,如有问题请留言. package simil