【讲解 + 模板】四种最短路算法的比较

四种最短路算法的比较

最短路

最短路,顾名思义,最短的路径。
我们把边带有权值的图称为带权图。边的权值可以理解为两点之间的距离。一张图中任意两点之间会有不同的路径相连。最短路径就是指连接两点的这些路径中最短的一条。
我们有四种算法可以有效地解决最短路径问题,但是当出现负边权时,有些算法不适用。

稠密图与稀疏图

有很少条边或弧(边的条数|E|远小于|V|²)的图称为稀疏图(sparse graph),反之边的条数|E|接近|V|²,称为稠密图(dense graph)。——摘自《百度百科》

依个人理解,相对于图中的点,边数较少的称为稀疏图,边数较多的称为稠密图。
这里写图片描述
由于各种算法对边和点的处理不尽相同,所以我们可以根据图的不同来选择不同的建图方式与最短路算法。

单源最短路径算法与多源最短路径算法

单源最短路径算法是用来计算从一个点到其他所有点的最短路径算法;
多源最短路径算法是用来计算任意两点之间的最短路径算法。

以下若无特别说明,dis[i][j]代表从i到j的最短路径,w[i][j]代表连接i,j的边的长度。

Floyed-Warshall算法 O(N^3)

分类:

多源最短路径算法。

作用:

1.求最短路。 2.判断一张图中的两点是否相连。

优点:

实现极为简单

缺点:

只有数据规模较小且时空复杂度都允许时才可以使用(NOIP上大概不会放出来的吧)。

思想:

3层循环,第一层枚举中间点k,第二层与第三层枚举两个端点i,j。若有dis[i][j] > dis[i][k] + dis[k][j] 则把dis[i][j]更新成dis[i][k] + dis[k][j](原理还是很好理解的)。

实现:

(a)初始化:点i,j如果有边相连,则dis[i][j] = w[i][j]。如果不相连,则dis[i][j] = 0x7fffffff(int极限值),表示两点不相连(或认为相隔很远)。
(b)算法代码:

for(int k = 1; k <= n; k++)  //枚举中间点(必须放最外层)
  for(int i = 1; i <= n; i++)  //枚举端点i
    if(i != k)
      for(int j = 1; j <= n; j++)  //枚举端点j
        if(i != j && j != k && dis[i][j] > dis[i][k] + dis[k][j])
          dis[i][j] = dis[i][k] + dis[k][j];

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

Floyed算法变形:

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

for(int k = 1; k <= n; k++)  //枚举中间点
  for(int i = 1; i <= n; i++)  //枚举端点i
    if(i != k)
      for(int j = 1; j <= n; j++)  //枚举端点j
        if(i != j && j != k)
          dis[i][j] = dis[i][j] || (dis[i][k] && dis[k][j]);(判断是否相连)

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

Dijkstra迪杰斯特拉算法 O(N^2)

分类:

单源最短路径算法。

适用于:

稠密图(侧重对点的处理)。

时间复杂度:

1.朴素:O(N^2)
2.堆优化:O(n * logn)

缺点:

不能处理存在负边权的情况。

算法思想:

把点分为两类,一类是已经确定最短路径的点,称之为“标记点”;另一类则是还未确定最短路径的点,称之为“未标记点”。如果要求出一个点的最短路径,就是把这个“未标记点”变成“标记点”,从起点到“未标记点”的最短路径上的中转点在这个时刻只能是“标记点”。
Dijkstra的算法思想,就是一开始将起点到起点的距离标记为0,而后进行n次循环,每次找出一个到起点距离dis[u]最短的点u,将它从“未标记点”变为“标记的点”。随后枚举所有的“未标记的点”vi,如果以此“标记的点”为中转点到达“未标记的点”vi的路径dis[u] + w[u][vi]更短的话,这将它作为vi的“更短路径”dis[vi](此时还不确定是不是vi的最短路径)。
就这样,我们每找到一个“标记的点”,就尝试着用它修改其他所有的:“未标记的点”,故每一个终点一定能被它的最后一个中转点所修改,而求得最短路径。

优化思想:

利用堆(优先队列),把冗杂的枚举查找变成更加快速的堆直接弹出。

实现思路:

(a)初始化:dis[v] = oo (v != s); dis[s] = 0; pre[s] = 0;
(b)for(int 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;
  }

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

更多关于此算法及其优化的详解请参照

Dijkstra迪杰斯特拉+堆优化

Bellman-Ford算法 O(NE)

分类:

单源最短路径算法。

适用于:

稀疏图(侧重于对边的处理)。

优点:

可以求出存在负边权情况下的最短路径。

缺点:

无法解决存在负权回路的情况。

时间复杂度:

O(NE),N是顶点数,E是边数。(因为和边有关,所以不适于稠密图)

算法思想:

很简单。一开始认为起点是“标记点”(dis[1] = 0),每一次都枚举所有的边,必然会有一些边,连接着“未标记的点”和“已标记的点”。因此每次都能用所有的“已标记的点”去修改所有的“未标记的点”,每次循环也必然会有至少一个“未标记的点”变为“已标记的点”。

算法实现:

初始化:dis[s] = 0; dis[v] = oo(v != s); pre[s] = 0;

(伪代码)
for(int i = 1; i <= n - 1; i++)
  for(int j = 1; j <= E; j++)  //注意要枚举所有边,不能枚举点
    if(dis[u] + w[j] < dis[v])  //u, v分别是这条边连接的两个点
      {
        dis[v] = dis[u] + w[j]
        pre[v] = u;
      }

SPFA算法O(KE)

适用于:

稀疏图(侧重于对边的处理)。

时间复杂度:

O(KE),K是常数,平均值为二,E是边数。(因为和边有关,所以不适于稠密图)

来源:

SPFA是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。
这个算法简单地说就是队列优化的Bellman-Ford,利用了每个点不会更新次数太多的特点发明的此算法。
SPFA在形式上和广度优先搜索非常类似,不同的是广度优先搜索中的一个点出了队列就不可能重新进入队列,但是SPFA中的一个点可能在出队列之后再次被放入队列,也就是说一个点修改过其他的点之后,过了一段时间可能会获得更短的路径,于是再次用来修改其他的点,这样反复进行下去。

优化方法:

1.循环队列(可以降低队列大小)
2.SLF:Small Label First 策略,设要加入的节点是j,队首元素为i,若dist(j) < dist(i),则将j插入队首,否则插入队尾。

if(!vis[temp])
{
    if(dis[q[head + 1]] < dis[temp])  //注意小于号不要写反,否则时间会爆
      {
        tail = (++tail - 1) % qxun + 1;
        q[tail] = temp;
      }
    else
      {
        q[head] = temp;
        if(--head == 0) head = qxun;
      }
    vis[temp] = 1;
}

3.LLL:Large Label Last 策略,设队首元素为i,每次弹出时进行判断,队列中所有dist值的平均值为x,若dist(i)>x则将i插入到队尾,查找下一元素,直到找到某一i使得dist(i)<=x,则将i出对进行松弛操作。

实现:

(伪代码)
dis[i]记录从起点s到i的最短路径,w[i][j]记录连接i,j的边的长度,pre[v]记录前趋。
team[1..n]为队列,头指针head,尾指针tail。
布尔数组exist[1..n]记录一个点是否现在存在队列中。
初始化:dis[s] = 0, dis[v] = oo(v != s), memset(exist, false, sizeof(exist));
起点入队 team[1] = s; head = 0; tail = 1; exist[s] = true;
do
  {
    1.头指针向下移一位,取出指向的点u。
    2.exist[u] = false; 已经被取出了队列。
    3.for与u相连的所有点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);

小结

如不能有效判断一个图是否稠密,建议使用堆优化迪杰斯特拉而不是SPFA,SPFA的时间复杂度不稳定且非常玄学。

本博文参考文献《信息学奥赛一本通:c++版/董永建著》

本篇博文较为粗略的介绍了四种最短路径算法及其在处理不同图时的优劣,疏漏或错误之处在所难免,敬请谅解。(打了好久字的说QAQ)
博文修改记录:

  1. 2017.11.8 增加SLF优化模板

猜你喜欢

转载自blog.csdn.net/Mashiro_ylb/article/details/78289790