最短路

最短路

一、求出最短路径的长度

  以下没有特别说明的话,dis[u][v]表示从uv最短路径长度,w[u][v]表示连接uv的边的长度。

1.Floyed-Warshall算法 O(N3)

  简称Floyed(弗洛伊德)算法,是最简单的最短路径算法,可以计算图中任意两点间的最短路径。Floyed的时间复杂度是O (N3),适用于出现负边权的情况。

算法描述:

       初始化:点uv如果有边相连,则dis[u][v]=w[u][v]

  如果不相连则dis[u][v]=0x7fffffff

For (k = 1; k <= n; k++)

    For (i = 1; i <= n; i++)

 For (j = 1; j <= n; j++)

     If (dis[i][j] >dis[i][k] + dis[k][j])

         dis[i][j] = dis[i][k] + dis[k][j];

        算法结束:dis[i][j]得出的就是从ij的最短路径。

算法分析&思想讲解:

  三层循环,第一层循环中间点k,第二第三层循环起点终点ij,算法的思想很容易理解:如果点i到点k的距离加上点k到点j的距离小于原先点i到点j的距离,那么就用这个更短的路径长度来更新原先点i到点j的距离。

  在上图中,因为dis[1][3]+dis[3][2]<dis[1][2],所以就用dis[1][3]+dis[3][2]来更新原先12的距离。

  我们在初始化时,把不相连的点之间的距离设为一个很大的数,不妨可以看作这两点相隔很远很远,如果两者之间有最短路径的话,就会更新成最短路径的长度。Floyed算法的时间复杂度是O(N3) Floyed算法变形:

  如果是一个没有边权的图,把相连的两点间的距离设为dis[i][j]=true,不相连的两点设为dis[i][j]=false,用Floyed算法的变形:

For (k = 1; k <= n; k++)

  For (i = 1; i <= n; i++)

    For (j = 1; j <= n; j++)

    dis[i][j] = dis[i][j] || (dis[i][k] && dis[k][j]);

       用这个办法可以判断一张图中的两点是否相连。

最后再强调一点:用来循环中间点的变量k必须放在最外面一层循环。

 

2.Dijkstra算法O (N2)

用来计算从一个点到其他所有点的最短路径的算法,是一种单源最短路径算法。也就是说,只能计算起点只有一个的情况。

Dijkstra的时间复杂度是O (N2),它不能处理存在负边权的情况。

算法描述:

       设起点为sdis[v]表示从sv的最短路径,pre[v]v的前驱节点,用来输出路径。

       1)初始化:dis[v]=(vs); dis[s]=0; pre[s]=0;

       2For (i = 1; i <= n ; i++)

            1.在没有被访问过的点中找一个顶点u使得dis[u]是最小的。

            2.u标记为已确定最短路径

            3.For u相连的每个未确定最短路径的顶点v

              if  (dis[u]+w[u][v] < dis[v])

               {dis[v] = dis[u] + w[u][v];pre[v] = u;}

       3)算法结束:dis[v]sv的最短距离;pre[v]v的前驱节点,用来输出路径。

算法分析&思想讲解:

从起点到一个点的最短路径一定会经过至少一个“中转点”(例如下图15的最短路径,中转点是2。特殊地,我们认为起点1也是一个“中转点”)。显而易见,如果我们想求出起点到一个点的最短路径,那我们必然要先求出中转点的最短路径(例如我们必须先求出点2 的最短路径后,才能求出从起点到5的最短路径)。

换句话说,如果起点1到某一点V0的最短路径要经过中转点Vi,那么中转点Vi一定是先于V0被确定了最短路径的点。

 我们把点分为两类,一类是已确定最短路径的点,称为“白点”,另一类是未确定最短路径的点,称为“蓝点”。如果我们要求出一个点的最短路径,就是把这个点由蓝点变为白点。从起点到蓝点的最短路径上的中转点在这个时刻只能是白点。

 Dijkstra的算法思想,就是一开始将起点到起点的距离标记为0,而后进行n次循环,每次找出一个到起点距离dis[u]最短的点u,将它从蓝点变为白点。随后枚举所有的蓝点vi,如果以此白点为中转到达蓝点vi的路径dis[u]+w[u][vi]更短的话,这将它作为vi的“更短路径”dis[vi](此时还不确定是不是vi的最短路径)。

    就这样,我们每找到一个白点,就尝试着用它修改其他所有的蓝点。中转点先于终点变成白点,故每一个终点一定能够被它的最后一个中转点所修改,而求得最短路径。

 

3.Bellman-Ford算法O(NE)

简称Ford(福特)算法,同样是用来计算从一个点到其他所有点的最短路径的算法,也是一种单源最短路径算法。能够处理存在负边权的情况,但无法处理存在负权回路的情况(下文会有详细说明)。

算法时间复杂度:O(NE)N是顶点数,E是边数。

算法实现:

s为起点,dis[v]即为sv的最短距离,pre[v]v前驱。w[j]是边j的长度,且j连接uv

初始化:dis[s]=0,dis[v]=∞(vs),pre[s]=0

For (i = 1; i <= n-1; i++)

For (j = 1; j <= E; j++)        //注意要枚举所有边,不能枚举点。

if (dis[u]+w[j]<dis[v])  //uv分别是这条边连接的两个点。

{dis[v] =dis[u] + w[j];pre[v] = u;}

算法分析&思想讲解:

Bellman-Ford算法的思想很简单。一开始认为起点是白点(dis[1]=0),每一次都枚举所有的边,必然会有一些边,连接着白点和蓝点。因此每次都能用所有的白点去修改所有的蓝点,每次循环也必然会有至少一个蓝点变成白点。

负权回路:

虽然Bellman-Ford算法可以求出存在负边权情况下的最短路径,却无法解决存在负权回路的情况。

 负权回路是指边权之和为负数的一条回路,上图中----②这条回路的边权之和为-3。在有负权回路的情况下,从16的最短路径是多少?答案是无穷小,因为我们可以绕这条负权回路走无数圈,每走一圈路径值就减去3,最终达到无穷小。

所以说存在负权回路的图无法求出最短路径,Bellman-Ford算法可以在有负权回路的情况下输出错误提示。

 如果在Bellman-Ford算法的两重循环完成后,还是存在某条边使得:dis[u]+w<dis[v],则存在负权回路:

For每条边(u,v)

    If  (dis[u]+w<dis[v])  return False

如果我们规定每条边只能走一次,在这个前提下可以求出负权回路的最短路径。这个问题就留待读者自己思考(提示:对Floyed做一点小处理)。

 

 

4SPFA算法O(kE)

SPFABellman-Ford算法的一种队列实现,减少了不必要的冗余计算。

主要思想:

初始时将起点加入队列。每次从队列中取出一个元素,并对所有与它相邻的点进行修改,若某个相邻的点修改成功,则将其入队。直到队列为空时算法结束。

这个算法,简单的说就是队列优化的bellman-ford,利用了每个点不会更新次数太多的特点发明的此算法。

SPFA 在形式上和广度优先搜索非常类似,不同的是广度优先搜索中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其它的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其它的点,这样反复进行下去。

算法时间复杂度:O(kE)E是边数。K是常数,平均值为2

算法实现:

    dis[i]记录从起点si的最短路径,w[i][j]记录连接ij的边的长度。pre[v]记录前趋。team[1..n]为队列,头指针head,尾指针tail。布尔数组exist[1..n]记录一个点是否现在存在在队列中。

    初始化:dis[s]=0,dis[v]=∞(vs),memset(exist,false,sizeof(exist));

    起点入队team[1]=s; head=0; tail=1;exist[s]=true;

    do

    {

 1、头指针向下移一位,取出指向的点u

 2exist[u]=false;已被取出了队列

 3foru相连的所有点v          

//注意不要去枚举所有点,用数组模拟邻接表存储

              if (dis[v]>dis[u]+w[u][v])

                 {

                      dis[v]=dis[u]+w[u][v];pre[v]=u;

                     if (!exist[v]) //队列中不存在v点,v入队。

                       {//尾指针下移一位,v入队;exist[v]=true;}

                 

    }while (head < tail);

循环队列:

  采用循环队列能够降低队列大小,队列长度只需开到2*n+5即可。例题中的参考程序使用了循环队列。

 

二、输出最短路径

1.单源最短路径的输出

DijkstraBellman-FordSPFA都是单源最短路径算法,它们的共同点是都有一个数组pre[x] 用来记录从起点到x的最短路径中,x的前驱结点是哪个。每次更新,我们就给pre[x]赋一个新值,结合上面的思想讲解,相信对于记录某点的前驱结点是不难理解的。那么怎么利用pre[x]数组输出最短路径方案呢?

感谢各位与信奥一本通的鼎力相助!

posted @ 2019-06-04 19:46  SeanOcean  阅读(296)  评论(0编辑  收藏  举报