zoukankan      html  css  js  c++  java
  • SPFA 最短路

        求单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm。 
        SPFA算法是西南交通大学段凡丁于1994年发表的。
        从名字我们就可以看出,这种算法在效率上一定有过人之处。 
        很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。有人称spfa算法是最短路的万能算法。

        简洁起见,我们约定有向加权图G不存在负权回路,即最短路径一定存在。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路。
        我们用数组dis记录每个结点的最短路径估计值,可以用邻接矩阵或邻接表来存储图G,推荐使用邻接表。

    spfa的算法思想(动态逼近法):
        设立一个先进先出的队列q用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。 
        松弛操作的原理是著名的定理:“三角形两边之和大于第三边”,在信息学中我们叫它三角不等式。所谓对结点i,j进行松弛,就是判定是否dis[j]>dis[i]+w[i,j],如果该式成立则将dis[j]减小到dis[i]+w[i,j],否则不动。 
        下面举一个实例来说明SFFA算法是怎样进行的:




    和广搜bfs的区别:
        SPFA 在形式上和广度(宽度)优先搜索非常类似,不同的是bfs中一个点出了队列就不可能重新进入队列,但是SPFA中一个点可能在出队列之后再次被放入队列,也就是一个点改进过其它的点之后,过了一段时间可能本身被改进(重新入队),于是再次用来改进其它的点,这样反复迭代下去。

    判断有无负环:

      如果某个点进入队列的次数超过N次则存在负环(SPFA无法处理带负环的图)

     算法的描述:

     1 void  spfa(s);  //求单源点s到其它各顶点的最短距离
     2     for i=1 to n do { dis[i]=∞; vis[i]=false; }   //初始化每点到s的距离,不在队列
     3     dis[s]=0;  //将dis[源点]设为0
     4     vis[s]=true; //源点s入队列
     5     head=0; tail=1; q[tail]=s; //源点s入队, 头尾指针赋初值
     6     while head<tail do {
     7        head+1;  //队首出队
     8        v=q[head];  //队首结点v
     9        vis[v]=false;  //释放对v的标记,可以重新入队
    10        for 每条边(v,i)  //对于与队首v相连的每一条边
    11         if (dis[i]>dis[v]+a[v][i])  //如果不满足三角形性质
    12          dis[i] = dis[v] + a[v][i]   //松弛dis[i]
    13         if (vis[i]=false) {tail+1; q[tail]=i; vis[i]=true;} //不在队列,则加入队列
    14     } 
    具体代码实现:
    /*********************************************/  
    //  d数组类似迪杰斯特拉的dis数组,记录起点到i点的局部最优解  
    //  c数组用来记录访问 i 点的次数  
    //  vis 记录是否在队列里面  ,与dijkstra中的s数据作用不同
    //  用数组模拟邻接表存图,w数组为权值  
    /*********************************************/  
    bool spfa_bfs(int s) // s为图的起点  
    {  
        queue <int> q; //  队列里存点  
        memset(d,0x3f,sizeof(d));    
        memset(c,0,sizeof(c));  
        memset(vis,0,sizeof(vis));  
        q.push(s);  
        vis[s]=1;  
        c[s]=1;  
        d[s]=0;  
        //顶点入队vis要做标记,另外要统计顶点的入队次数  
        while(!q.empty())  
        {  
            int x;  
            x=q.front();  
            q.pop();  
            vis[x]=0;  
            //队头元素出队,并且消除标记  
            for(int k=f[x]; k!=0; k=nnext[k]) //遍历顶点x的邻接表  
            {  
                int y=v[k];  
                if( d[x]+w[k] < d[y]) //如果可以松弛  
                {  
                    d[y]=d[x]+w[k];  //松弛  
                    if(!vis[y])  //顶点y不在队内  不要重复入队列  
                    {  
                        vis[y]=1;    //标记  
                        c[y]++;      //统计次数  
                        q.push(y);   //入队  
                        if(c[y]>NN)  //超过入队次数上限,说明有负环  
                            return false;  
                    }  
                }  
            }  
        }  
        return true;  
    
    最短路径本身怎么输出?
        在一个图中,我们仅仅知道结点A到结点E的最短路径长度,有时候意义不大。这个图如果是地图的模型的话,在算出最短路径长度后,我们总要说明“怎么走”才算真正解决了问题。如何在计算过程中记录下来最短路径是怎么走的,并在最后将它输出呢?
        我们定义一个path[]数组,path[i]表示源点s到i的最短路程中,结点i之前的结点的编号(父结点),我们在借助结点u对结点v松弛的同时,标记下path[v]=u,记录的工作就完成了。
        如何输出呢?我们记录的是每个点前面的点是什么,输出却要从最前面到后面输出,这很好办,递归就可以了: 
     1 c++ code:
     2 void printpath(int k){
     3     if (path[k]!=0) printpath(path[k]);
     4     cout << k << ' ';
     5 }
     6 
     7 pascal code:
     8 procedure printpath(k:longint);
     9   begin
    10     if path[k]<>0 then printpath(path[k]);
    11     write(k,' ');
    12   end;
    13 
    14 spfa算法模板(邻接矩阵):
    15 c++ code:
    16 void spfa(int s){
    17     for(int i=0; i<=n; i++) dis[i]=99999999; //初始化每点i到s的距离
    18     dis[s]=0; vis[s]=1; q[1]=s;  队列初始化,s为起点
    19     int i, v, head=0, tail=1;
    20     while (head<tail){   队列非空
    21         head++; 
    22         v=q[head];  取队首元素
    23         vis[v]=0;   释放队首结点,因为这节点可能下次用来松弛其它节点,重新入队
    24         for(i=0; i<=n; i++)  对所有顶点
    25            if (a[v][i]>0 && dis[i]>dis[v]+a[v][i]){  
    26                 dis[i] = dis[v]+a[v][i];   修改最短路
    27                 if (vis[i]==0){  如果扩展结点i不在队列中,入队
    28                     tail++;
    29                     q[tail]=i;
    30                     vis[i]=1;
    31                 }
    32            }
    33         
    34     }
    35 }
    36 
    37 pascal code:
    38 procedure spfa(s:longint);
    39   var i,j,v,head,tail:longint;
    40   begin
    41     for i:=0 to n do dis[i]:=99999999;
    42     dis[s]:=0; vis[s]:=true; q[1]:=s;
    43     head:=0;tail:= 1;
    44     while head<tail do
    45        begin
    46          inc(head);
    47          v:=q[head];
    48          vis[v]:=false;
    49          for i:=0 to n do
    50            if dis[i]>dis[v]+a[v,i] then
    51              begin
    52                dis[i]:= dis[v]+a[v,i];
    53                if not vis[i] then
    54                  begin
    55                    inc(tail);
    56                    q[tail]:=i;
    57                    vis[i]:=true;
    58                  end;
    59              end;
    60 
    61       end;
    62   end; 

    spfa优化——深度优先搜索dfs

             在上面的spfa标准算法中,每次更新(松弛)一个结点u时,如果该结点不在队列中,那么直接入队。
        但是有负环时,上述算法的时间复杂度退化为O(nm)。能不能改进呢?
        那我们试着使用深搜,核心思想为每次从更新一个结点u时,从该结点开始递归进行下一次迭代。
    使用dfs优化spfa算法:
     1 pascal code:
     2 procedure spfa(s:longint);
     3   var i:longint;
     4   begin
     5     for i:=1 to b[s,0] do  //b[s,0]是从顶点s发出的边的条数
     6            if dis[b[s,i]]>dis[s]+a[s,b[s,i]] then  //b[s,i]是从s发出的第i条边的另一个顶点
     7              begin
     8                dis[b[s,i]]:=dis[s]+a[s,b[s,i]];
     9                spfa(b[s,i]);
    10              end;
    11   end; 
    12 
    13 C++ code:
    14 void spfa(int s){
    15     for(int i=1; i<=b[s][0]; i++)  //b[s,0]是从顶点s发出的边的条数
    16        if (dis[b[s][i]>dis[s]+a[s][b[s][i]]){  //b[s,i]是从s发出的第i条边的另一个顶点
    17         dis[b[s][i]=dis[s]+a[s][b[s][i]];
    18         spfa(b[s][i]);
    19        }
    20 }
             相比队列,深度优先搜索有着先天优势:在环上走一圈,回到已遍历过的结点即有负环。绝大多数情况下的时间复杂度为O(m)级别。
        那我们试着使用深搜,核心思想为每次从更新一个结点u时,从该结点开始递归进行下一次迭代。
        对于WorldRings(ACM-ICPC Centrual European 2005)这道题,676个点,100000条边,查找负环dfs仅仅需219ms。
        一个简洁的数据结构和算法在一定程度上解决了大问题。
    判断存在负环的条件:重新经过某个在当前搜索栈中的结点。
     
    spfa优化——前向星优化
             星形(star)表示法的思想与邻接表表示法的思想有一定的相似之处。对每个结点,它也是记录从该结点出发的所有弧,但它不是采用单向链表而是采用一个单一的数组表示。也就是说,在该数组中首先存放从结点1出发的所有弧,然后接着存放从节点2出发的所有孤,依此类推,最后存放从结点n出发的所有孤。对每条弧,要依次存放其起点、终点、权的数值等有关信息。这实际上相当于对所有弧给出了一个顺序和编号,只是从同一结点出发的弧的顺序可以任意排列。此外,为了能够快速检索从每个节点出发的所有弧,我们一般还用一个数组记录每个结点出发的弧的起始地址(即弧的编号)。在这种表示法中,可以快速检索从每个结点出发的所有弧,这种星形表示法称为前向星形(forward star)表示法。
        例如,在下图中,仍然假设弧(1,2),(l,3),(2,4),(3,2),(4,3),(4,5),(5,3)和(5,4)上的权分别为8,9,6,4,0,7,6和3。此时该网络图可以用前向星形表示法表示如下:
      
    前向星存储图:
     1 #include <iostream>
     2 using namespace std;
     3 int first[10005];
     4 struct edge{
     5     int point,next,len;
     6 } e[10005];
     7 void add(int i, int u, int v, int w){
     8         e[i].point = v;
     9         e[i].next = first[u];
    10         e[i].len = w;
    11         first[u] = i;
    12 }
    13 int n,m;
    14 int main(){
    15     int u,v,w;
    16     cin >> n >> m;
    17     for (int i = 1; i <= m; i++){
    18         cin >> u >> v >> w;
    19         add(i,u,v,w);
    20     }  //这段是读入和加入
    21     for (int i = 0; i <= n; i++){
    22         cout << "from " << i << endl;
    23         for (int j = first[i]; j; j = e[j].next)  //这就是遍历边了
    24             cout << "to " << e[j].point << " length= " << e[j].len << endl;
    25     }
    26 }

     来自资料:

    http://blog.csdn.net/WR_technology/article/details/51254054

    http://blog.csdn.net/xunalove/article/details/70045815

  • 相关阅读:
    Atitit sql计划任务与查询优化器统计信息模块
    Atitit  数据库的事件机制触发器与定时任务attilax总结
    Atitit 图像处理知识点体系知识图谱 路线图attilax总结 v4 qcb.xlsx
    Atitit 图像处理 深刻理解梯度原理计算.v1 qc8
    Atiti 数据库系统原理 与数据库方面的书籍 attilax总结 v3 .docx
    Atitit Mysql查询优化器 存取类型 范围存取类型 索引存取类型 AND or的分析
    Atitit View事件分发机制
    Atitit 基于sql编程语言的oo面向对象大规模应用解决方案attilax总结
    Atitti 存储引擎支持的国内点与特性attilax总结
    Atitit 深入理解软件的本质 attilax总结 软件三原则"三次原则"是DRY原则和YAGNI原则的折
  • 原文地址:https://www.cnblogs.com/curo0119/p/8515811.html
Copyright © 2011-2022 走看看