图论 之 最短路

最短路问题(short-path problem)

若网络中的每条边都有一个数值(长度、成本、时间等),则找出两节点(通常是源节点和阱节点)之间总权和最小的路径就是最短路问题。最短路问题是网络理论解决的典型问题之一,可用来解决管路铺设、线路安装、厂区布局和设备更新等实际问题。

最短路问题,我们通常归属为三类:

单源最短路径问题
    ——包括确定起点的最短路径问题,确定终点的最短路径问题(与确定起点的问题相反,该问题是已知终结结点,求最短路径的问题。在无向图中该问题与确定起点的问题完全等同,在有向图中该问题等同于把所有路径方向反转的确定起点的问题。) 算法可以采用Dijkstra算法
 
确定起点终点的最短路径问题
  ——即已知起点和终点,求两结点之间的最短路径。
 
全局最短路径问题
   ——求图中所有的最短路径。算法可以采用Floyd-Warshall算法。如果图中有负权回路,可以采用Bellman-Ford算法
 
以上文本内容来自百度百科。
---------------------------------------------------------------------------
最短路问题是图论里面最简单易学的,也是比较基础的。
前几天一直想写最短路的小结的,懒掉了,博客文章一直没有更新。
学习最短路时学习图论的开始,还是应该纪念下的! ^^
merlininice师父当年教的时候说:你去搜几道最短路的题目,然后用dijkstra基础的写一遍,过掉,然后再用dijkstra+优先队列的写一遍,过掉,然后再用spfa写一遍,过掉,就好了!(= =||) 郁闷的是,我真的照做了... 呜呜呜,最奇怪的是,第二天晚上他还主动跑过来问,最短路写的怎么样了... 由此看来最短路还是应该好好学习的!(因为我师父向来半酱油党...)   
以上内容纯粹扯淡...
 
这里涉及的最短路时最基础的算法,下面我会总结dijkstra基础算法(O(n^2)),dijkstra+优先队列优化算法、spfa和floyed算法,最短路基本掌握这3种算法也算是真正入门了。
 
(一)、dijkstra基础算法
           基础算法也是最原始的算法,只要理解了这个算法的核心要义,代码很快就出来了,顺带提及下,dijkstra属于贪心算法一块。
 
           Dijkstra(迪杰斯特拉)算法是典型的单源最短路径算法,用于计算一个节点到其他所有节点的最短路径。主要特点是以起始点为中心向外层层扩展,直到扩展到终点为止。Dijkstra算法是很有代表性的最短路径算法,在很多专业课程中都作为基本内容有详细的介绍,如数据结构,图论,运筹学等等。Dijkstra一般的表述通常有两种方式,一种用永久和临时标号方式,一种是用OPEN, CLOSE表的方式,这里均采用永久和临时标号的方式。注意该算法要求图中不存在负权边。
                                                                   ——摘自百度百科
           dijkstra就是以起点为中心,第一次向外寻找与起点V0直接距离最小的点V1,记录这个最小的距离W1,接下来就是一个贪心的过程:认为再也不可能通过其他路径,使得从V0到V1距离比W1距离还要小。(反证即可证明)于是我们第一次得到了V1点和W1。
           第二次通过V1点更新起点V0到其他所有点的距离,同样,我们接着上述步骤再寻找到达V0的最短距离。显然,这里我们需要多做一些动作,就是标记V1已经访问过了,因为接下来无论怎么更新,都不可能有比W1更短的路径了,假使不更新的话,岂不是陷入死循环了? 于是我们又得到了V2和W2...
          …… …… …… 依次类推,当所有顶点都被访问过后,我们也就得到了V0分别到所有其他顶点的最短路了。
          例子更能帮助理解:
         

  第一次: 沿着深红色路径先找到V1,W1= 3;

  第二次: 由V1更新V0到其他顶点的距离,沿着黄色路径找到V2,W2= 3+2=5;

  第三次: 由V2更新V0到其他顶点的距离,沿着蓝色路径找到V3=10(因为V2拓展出的路径长度都大于10);

  第四次: 由V3更新V0到其他顶点的距离,沿着褐色路径找到V4,W4= 10+1=11;

  第五次:有V4更新V0到其他顶点的距离,沿着粉色路径找到V5,W5 = 10+3;

这样下来所有顶点都已经访问完毕,算法结束。

 

整理代码:

      我们发现整个过程其实就是以起点开始,每次在未访问过的顶点中寻找起点到该点距离最短的,并且以这个点来更新其他顶点,然后循环n-1次(因为顶点不需要操作么~)

那么代码也就很显然了:

 1 void dij()   //matrix数组存放i到j的距离,不可达为无穷大
 2 {
 3     int i, j, minx, pos;
 4     for(i=1; i<=n; ++i){
 5         vist[i] = false;      //vist数组记录顶点是否被访问过
 6         d[i] = matrix[1][i];   //d数组是dijkstra的核心
 7     }
 8 
 9     vist[1] = 1;    //初始化默认起点被访问过
10     for(i=1; i<n; ++i){    //遍历n-1次
11            minx = inf;  
12            for(j=1; j<=n; ++j)if(!vist[j]){   //每次从未被访问过的顶点中寻找距离最小的
13                  if(minx > d[j]){
14                         minx = d[j];
15                         pos = j;
16                  }
17            }
18            vist[pos] = 1;  //标记这个顶点被访问过
19            for(j=1; j<=n; ++j){     //更新其他顶点到起点的距离
20                  if(!vist[j] && matrix[pos][j] && minx+matrix[pos][j]<d[j])
21                          d[j] = minx+matrix[pos][j];
22            }
23     }
24 }

 

(二)、dijkstra+优先队列

           学习完优先队列是很早之前,那个时候merlininice师父坚持要我学习优先队列,其实也就是学习了STL的优先队列的用法而已,不过,真的挺好用的! 而且用在dijkstra算法上我觉得很贴切!

          了解了上述的过程,想必你会觉得,咦,每次不是都是寻找距离起点最短的顶点嘛?(从未标记的点中)

          Bingo!

          因此我们借助priority_queue这个武器不是既优化了时间效率(因为他的查找是O(logn)的),又简洁了代码。

具体如下:

 1 vector <int> e[MAXN];
 2 vector <int> l[MAXN];    //用邻接表存储
 3 
 4 void Initial()  //邻接表的初始化
 5 {
 6     for(int i=0; i<MAXN; ++i){
 7          e[i].clear();
 8          l[i].clear();
 9     }
10 }
11 
12 struct node   //dis代表顶点标号为id的点到起点的距离
13 {
14     int dis, id;
15     bool operator < (const node &x) const{  //重载小于号
16           return dis>x.dis;
17     }
18 };
19 
20 int dij(int n)
21 {
22     priority_queue<node>que;  //创建优先队列
23     int i, j, dis, id;
24     node t, nt;
25 
26     for(i=1; i<=n; ++i){    //d是核心!
27           vist[i]  = false;   //vist代表顶点有没有被访问过
28           d[i] = inf;
29     }
30 
31     for(i=0; i<e[1].size(); ++i){  //将与起点直接相连的点压入优先队列中
32           t.id = e[1][i];
33           t.dis = l[1][i];
34           d[e[1][i]] = t.dis;
35           que.push(t);
36     }
37 
38     vist[1] = true;
39     while(!que.empty()){  
40              t = que.top();   //取出于起点距离最小的点
41              id = t.id;
42              dis = t.dis;
43              que.pop();
44              if(id==n) {  //如果已经是我们所要求的终点,返回(这里可做修改而实现其他的功能)
45                  return dis;
46              }
47              if(vist[id])  continue;  //假如是已经访问过的顶点,则不需要更新其他顶点
48              vist[id] = true;      //否则标记并更新其他顶点
49              for(i=0; i<e[id].size();++i){  
50                     if(d[id]+l[id][i]<d[e[id][i]]){
51                               d[e[id][i]] = d[id] + l[id][i];
52                               nt.id = e[id][i];
53                               nt.dis = d[e[id][i]];
54                               que.push(nt);
55                     }
56              }
57     }
58 }

   顺带提及下:在图的问题中,如果是稀疏、顶点数又较多的图,则建议用邻接表存储,防止mle或者数组开不下...

 

(三)、spfa

  求单源最短路的SPFA算法的全称是:Shortest Path Faster Algorithm。

  SPFA算法是西南交通大学段凡丁于1994年发表的.

  从名字我们就可以看出,这种算法在效率上一定有过人之处。

  很多时候,给定的图存在负权边,这时类似Dijkstra等算法便没有了用武之地,而Bellman-Ford算法的复杂度又过高,SPFA算法便派上用场了。

  简洁起见,我们约定有向加权图G不存在负权回路,即最短路径一定存在。当然,我们可以在执行该算法前做一次拓扑排序,以判断是否存在负权回路,但这不是我们讨论的重点。

  我们用数组d记录每个结点的最短路径估计值,而且用邻接表来存储图G。我们采取的方法是松弛:设立一个先进先出的队列用来保存待优化的结点,优化时每次取出队首结点u,并且用u点当前的最短路径估计值对离开u点所指向的结点v进行松弛操作,如果v点的最短路径估计值有所调整,且v点不在当前的队列中,就将v点放入队尾。这样不断从队列中取出结点来进行松弛操作,直至队列空为止。

——摘自百度百科

      摘用这段话的目的其实就是为了说明spfa是Shortest Path Faster Algorithm的缩写...(因为之前总是记岔了,被bs了n次...)

      spfa也是用队列来维护d数组(一般流行用dis数组~),不同于dijkstra+优先队列,spfa只需要用普通队列来维护即可。 他的原理是每次如果发现起点可以通过其他的点到达该点,并且路径比原路径短的话,就更新该点,并且该点有可能去更新其他的点。那么我们用一个队列将这些可能更新其他点的点保存下来,一直循环更新,直到所有的路径都已经不能被任何点更新了,也就是不存在可以更新其他点的点了,亦即队列为空的时候。

      spfa可以解决负权边的问题,这是dijkstra做不到的。(注意,图中肯定是没有负环的,如果存在负环,也就没有最短路这一个说法了~) 

 1 void spfa()
 2 {
 3     int i, j, v;
 4     queue <int> que;  //创建一个普通队列
 5     for(i=1; i<=n; ++i){   //初始化
 6           d[i] = inf;
 7           inque[i] = false;   //inque数组标记该点是否在队列中
 8     }
 9     d[a] = 0;  
10     inque[a] = true;
11     que.push(a);    //初始化将顶点推入队列中
12     while(!que.empty()){
13           v = que.front();  //推出队头
14           que.pop();
15           inque[v] =false;
16           for(i=0; i<e[v].size(); ++i){   //用队头顶点更新其他顶点
17                   if(d[e[v][i]] > d[v] +mat[v][e[v][i]]){
18                        d[e[v][i]] = d[v]+mat[v][e[v][i]];
19                        if(!inque[e[v][i]]){   //假如该顶点未在队列中,那么如队列
20                              inque[e[v][i]] = true;
21                              que.push(e[v][i]);
22                        }
23                   }
24           }
25     }
26 }

 

(三)、floyed

           floyed算法解决全局问题,其实就是dp的思想,很好理解,代码也很死。 自学之后终于明白为啥师父当年怎么也不肯讲floyed了,因为,真的很简单。

           算法的核心在于:寻找一个中转站,也就是i->j的最短路径必然是:Vi->....->Vj(....也可能是空),我们只要每次拿其他的点来更新i到j的路径就好了。

 1 void floyd(int n)
 2 {
 3     int i, j, k, tmp;
 4 
 5     for(i=1; i<=n; ++i)
 6           for(j=1; j<=n; ++j)
 7                 path[i][j] = j;
 8 
 9     for(k=1; k<=n; ++k)   //k是中转站,并且一定要放在外层!
10             for(i=1; i<=n; ++i)
11                    for(j=1; j<=n; ++j){
12                            if(d[i][k]!=-1 && d[k][j]!=-1){
13                                    tmp = d[i][k] + d[k][j] + tax[k];
14                                    if(tmp<d[i][j] || d[i][j]==-1){
15                                             d[i][j] = tmp;
16                                             path[i][j] = path[i][k];
17                                    }
18                                    if(tmp==d[i][j] && path[i][j]>path[i][k])
19                                           path[i][j] = path[i][k];
20                            }
21                    }
22 }

 

posted on 2012-08-28 21:54  Yuna_  阅读(292)  评论(0)    收藏  举报