zoukankan      html  css  js  c++  java
  • 图论——最短路:Floyd,Dijkstra,Bellman-Ford,SPFA算法及最小环问题

    一.Floyd算法

      用于计算任意两个节点之间的最短路径

            参考了five20的博客

            Floyd算法的基本思想如下:从任意节点A到任意节点B的最短路径不外乎2种可能,1是直接从A到B,2是从A经过若干个节点到B,所以,我们假设dist(AB)为节点A到节点B的最短路径的距离,对于每一个节点K,我们检查dist(AK) + dist(KB) < dist(AB)是否成立,如果成立,证明从A到K再到B的路径比A直接到B的路径短,我们便设置 dist(AB) = dist(AK) + dist(KB),这样一来,当我们遍历完所有节点K,dist(AB)中记录的便是A到B的最短路径的距离。

     标准五行代码如下:

    for(k=1;k<=n;k++)
        for(i=1;i<=n;i++)
            for(j=1;j<=n;j++) {
                if(dis[i][k]+dis[k][j]<dis[i][j]) {
                     dis[i][j]=dis[i][k]+dis[k][j];
        }
    } 

       但是这里我们要注意循环的嵌套顺序,如果把检查所有节点K放在最内层,那么结果将是不正确的,为什么呢?因为这样便过早的把i到j的最短路径确定下来了,而当后面存在更短的路径时,已经不再会更新了。

      

      更多关于Floyd算法,详细请见:Floyd算法百度百科链接

    二.Dijkstra算法:

      适用于权值为非负的图的单源最短路径

      斐波那契堆优化的时间复杂度为O(E+VlogV),但其实只是理论值,实际中基本上达不到。

      算法思路:

      参考了殷天文的博客

    • 指定一个节点,例如我们要计算 'A' 到其他节点的最短路径

    • 引入两个集合(S、U),S集合包含已求出的最短路径的点(以及相应的最短长度),U集合包含未求出最短路径的点(以及A到该点的路径,注意 如上图所示,A->C由于没有直接相连 初始时为∞

    • 初始化两个集合,S集合初始时 只有当前要计算的节点,A->A = 0

      U集合初始时为 A->B = 4, A->C = ∞, A->D = 2, A->E = ∞敲黑板!!!接下来要的两个步骤是核心!

    • 从U集合中找出路径最短的点,加入S集合,例如 A->D = 2

    • 更新U集合路径,if ( 'D 到 B,C,E 的距离' + 'AD 距离' < 'A 到 B,C,E 的距离' ) 则更新U

    • 循环执行 4、5 两步骤,直至遍历结束,得到A 到其他节点的最短路径

      朴素版时间复杂度O(n²)算法代码如下:

    void Dijkstra()
    {
        memset(dis, 0x1f, sizeof(dis));
        dis[1] = 0;
        for (int i=1; i<=n; i++) {
            int min_len = 1e9, k = 0;
            for (int j=1; j<=n; j++)     
                if (!vis[j] && dis[j] < min_len) {
                    min_len = dis[j];
                    k = j;
                }
            vis[k] = 1;     
            for (int j=h[k]; j!=-1; j=edge[j].next) {    
                int to = edge[j].to, w = edge[j].w;
                if (!vis[to] && dis[to] > dis[k]+w) {
                    dis[to] = dis[k]+w;
                }
            }
        }
    }

      稳定时间复杂度O(mlogn)的堆优化(优先队列代替)代码如下:

    void Dijkstra_Heap()
    {
        priority_queue<pair<int, int> > q;
        memset(dis, 0x3f, sizeof(dis));
        memset(vis,0,sizeof(v));
        dis[1] = 0; 
        q.push(make_pair(0, 1));
        while (!q.empty()) {
            int now = q.top().second;
            q.pop();
            if (vis[now]) continue;
            vis[now] = 1;
            for (int i=h[now]; i!=-1; i=edge[i].next) {
                int to = edge[i].to, w = edge[i].w;
                if (!vis[to] && dis[to]>dis[now]+w) {
                    dis[to] = dis[now] + w;
                    q.push(make_pair(-dis[to], to));
                }
            }
        }
    } 

    这里有一个关于优先队列的小骚操作

      由于STL中的优先队列默认是大根堆,所以在使用push函数的时候只需在需要排序的数据前加个‘-’即可。

     

      关于Dijkstra算法的选择上,对于稀疏图,由于n和m比较接近,故选择堆优化算法;而对于稠密图,由于点少边多,故选择朴素版算法

     

      更多关于Dijkstra算法,详细请见:Dijkstra算法百度百科链接

       推荐博客:数据结构--Dijkstra算法最清楚的讲解

    三.Bellman-Ford算法

      参考博客:图解贝尔曼福特-算法

    可用于解决以下问题:

    • 从A出发是否存在到达各个节点的路径(有计算出值当然就可以到达);
    • 从A出发到达各个节点最短路径(时间最少、或者路径最少等)
    • 图中是否存在负环路(权重之和为负数)
    • 有边数限制的最短路

      算法思路:

    1.  初始化时将起点s到各个顶点v的距离dist(s->v)赋值为∞,dist(s->s)赋值为0
    2.  后续进行最多n-1次遍历操作,对所有的边进行松弛操作,假设:
    3.  所谓的松弛,以边ab为例,若dist(a)代表起点s到达a点所需要花费的总数, dist(b)代表起点s到达b点所需要花费的总数,weight(ab)代表边ab的权重, 若存在:
    4.  (dist(a) +weight(ab)) < dist(b)
    5.  则说明存在到b的更短的路径,s->...->a->b,更新b点的总花费为(dist(a) +weight(ab)),父节点为a
    6.  遍历都结束后,若再进行一次遍历,还能得到s到某些节点更短的路径的话,则说明存在负环路

      思路上与狄克斯特拉算法(Dijkstra algorithm)最大的不同是每次都是从源点s重新出发进行"松弛"更新操作,而Dijkstra则是从源点出发向外扩逐个处理相邻的节点,不会去重复处理节点,这边也可以看出Dijkstra效率相对更高点。

       下面是有边数k限制的Bellman_ford算法模板:

    void Bellman_ford()
    {
        memset(dis,0x1f,sizeof(dis));
        dis[1]=0;
        for(int i=1;i<=k;i++)
        {
            memcpy(last,dis,sizeof(last));
            for(int j=1;j<=m;j++)
            {
                int u=edge[j].u,v=edge[j].v,w=edge[j].w;
                if(dis[v]>last[u]+w)dis[v]=last[u]+w;
            }
        }
    }

      

      更多关于Bellman-Ford算法,详细请见:Bellman-Ford算法百度百科链接

    四.SPFA算法

      SPFA是西安交通大学的段凡丁在1994年与《西安交通大学学报》中发表的“关于最短路径的SPFA快速算法”,他在里面说SPFA速度比Dijkstra快,且运行V次的SPFA速度比Floyd速度快。
      而事实证明SPFA算法是有局限的,他不适用于稠密图,对于特别情况的稠密图,SPFA复杂度和BellmanFord时间一样。

      适用范围:适用于权值有负值,且没有负圈的图的单源最短路径,论文中的复杂度O(kE),k为每个节点进入Queue的次数,且k一般<=2,但此处的复杂度证明是有问题的,其实SPFA的最坏情况应该是O(VE).

      注意:SPFA算法在网格图中非常慢

      算法思路:

      参考了小天位的博客

    •  SPFA(Shortest Path Faster Algorithm) [图的存储方式为邻接表]
    •  是Bellman-Ford算法的一种队列实现,减少了不必要的冗余计算。
    •  算法大致流程是用一个队列来进行维护。 初始时将源加入队列。 每次从队列中取出一个元素,并对所有与他相邻的点进行松弛,若某个相邻的点松弛成功,则将其入队。 直到队列为空时算法结束。它可以在O(kE)的时间复杂度内求出源点到其他所有点的最短路径,可以处理负边。
    • SPFA 在形式上和BFS非常类似,不同的是BFS中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进,于是再次用来改进其它的点,这样反复迭代下去。
    •  判断有无负环:如果某个点进入队列的次数超过V次则存在负环(SPFA无法处理带负环的图)。

      代码如下:

    void SPFA() {
        memset(dis, 0x3f, sizeof(dis));
        memset(vis, 0, sizeof(vis));
        queue<int> q;
        dis[1] = 0; vis[1] = 1;
        q.push(1);
        while (!q.empty()) {
            int now = q.front();
            q.pop(); vis[now] = 0;
            for (int i=h[now]; i!=-1; i=edge[i].next) {
                int to = edge[i].to, w = edge[i].w;
                if (dis[now]+w < dis[to]) {  //在队列中的点也可以进行松弛操作,故这里不需加!ifq[to]的条件,而需要加在下面。
                    dis[to] = dis[now]+w;
                    if (!vis[to]) q.push(to), vis[to] = 1;
                }
            }
        }
    }

      更多关于SPFA算法,详细请见:SPFA算法百度百科链接

    五.最小环问题

      解决思路:

      最小环就是指在一张图中找出一个环,使得这个环上的各条边的权值之和最小。在Floyed的同时,可以顺便算出最小环。

      记两点间的最短路为dis[i][j],g[i][j]为边<i,j>的权值。 

      一个环中的最大结点为k(编号最大),与它相连的两个点为i,j,这个环的最短长度为g[i][k]+g[k][j]+(i到j的路径中,所有结点编号都小于k的最短路径长度)。

      根据Floyed的原理,在最外层循环做了k-1次之后,dis[i][j]则代表了i到j的路径中,所有结点编号都小于k的最短路径。

      综上所述,该算法一定能找到图中最小环。

      代码如下:

      参考了Coder_YX的博客

     void floyd(){
         int MinCost = inf;
         for(int k=1;k<=n;k++){
             for(int i=1;i<k;i++)
                 for(int j=i+1;j<k;j++)
                     MinCost = min(MinCost,dis[i][j]+mp[i][k]+mp[k][j]);//更新k点之前枚举ij求经过ijk的最小环
             for(int i=1;i<=n;i++)
                 for(int j=1;j<=n;j++)
                     dis[i][j]=min(dis[i][j],dis[i][k]+dis[k][j]);      //更新k点
         }
         if(MinCost==inf)puts("It's impossible.");
         else printf("%d
    ",MinCost);
     }

    关于四种最短路算法的结论:

     参考了xiazdong的博客

    (1)当权值为非负时,用Dijkstra。
    (2)当权值有负值,且没有负圈,则用SPFA,SPFA能检测负圈,但是不能输出负圈。
    (3)当权值有负值,而且可能存在负圈,则用BellmanFord,能够检测并输出负圈。
    (4)SPFA检测负环:当存在一个点入队大于等于V次,则有负环。

    作者:玖梦
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须在文章页面给出原文链接,否则保留追究法律责任的权利。
  • 相关阅读:
    状态压缩 + 暴力 HDOJ 4770 Lights Against Dudely
    简单几何(推公式) UVA 11646 Athletics Track
    简单几何(四边形形状) UVA 11800 Determine the Shape
    简单几何(求交点) UVA 11437 Triangle Fun
    计算几何模板
    简单几何(相对运动距离最值) UVA 11796 Dog Distance
    简单几何(求划分区域) LA 3263 That Nice Euler Circuit
    覆盖的面积 HDU
    Desert King 最小比率生成树 (好题)
    约会安排 (区间合并)毒瘤题
  • 原文地址:https://www.cnblogs.com/ninedream/p/11186049.html
Copyright © 2011-2022 走看看