最短路算法总结(入门版)

最近花了大约一个月左右的时间集中刷了一些图论的题目。虽然收获了许多但是还是付出了很多作业没有做的代价0.0。在这里先把自己所做的关于最短路的基础算法来做一个总结,至少把学到的东西记录下来。

先说明一下在这里我们暂且认为n为图中顶点个数,m为图中边的个数,INF为极大值(可以是题目计算过程中不会的得到的一个大数字)。

然后说一下最近学到了什么吧。最初学习的是dij--O(n^2)的算法。这个在以前数据结构的时候就已经学习过了现在加强了一下,熟练掌握了算法思想然后会裸敲。接着当然就是dij--O(mlogn)的算法。了解算法思想并且明白了是如何优化的,然后会裸敲。接着是floyd算法这个最简单了只有几行代码。但是它题目中往往会结合一些动态规划来考察你,复杂度为O(n^3),还有就是关于bellmam-ford与SPFA是关于带负权边图上的最短路。前者的复杂度为O(nm)后者为O(kn)k为常数一般情况下小于2。

一、Dijkstra算法

首先讲一下关于dij算法的思想。首先dij算法使用的前提是图中不存在负权边

我们分三步来讲述dij:

(1)参数与返回值

dij算法是单元最短路所以我们需要告诉dij函数你的源点(s)是哪一个结点,然后函数执行完后dis数组中存放的就是s到图中所有结点的最短距离,如果不连通的话会返回极大值。

(2)初始化

在初始化过程中我们要定义vis数组--用来记录已经访问过的结点,并且清零。然后给dis数组赋初值INF(s点为0),表示初始情况下源点到除自己之外的所有结点都为无穷大。

(3)算法主体

我们执行n次循环,每次从dis数组中选出一个值最小的结点-标记此结点-对这个结点所连接的每一条边进行松弛

 

if(mindis+Map[min][j]<dis[j] && Map[min][j]!=INF && vis[j]==0)
  dis[j] = mindis+Map[min][j];

 

然后我们就可以给出算法的所有代码:

 

 1 /****************************************
 2      Dijkstra O(n^2) 单元最短路算法
 3      邻接矩阵        
 4      By 小豪                
 5 *****************************************/
 6 #include <iostream>
 7 #include <cstdio>
 8 #include <string.h>
 9 #define INF 0x3f3f3f3f
10 #define LEN 1010
11 using namespace std;
12 
13 int Map[LEN][LEN], dis[LEN], n, m;
14 
15 void Dijkstra(int s)
16 {
17     int vis[LEN] = {0};
18     for(int i=1; i<=n; i++)
19         dis[i] = INF;
20     dis[s] = 0;
21     for(int i=0; i<n ;i++)
22     {
23         int min, mindis = INF;
24         for(int j=1; j<=n; j++)
25             if(dis[j]<mindis && vis[j] == 0)
26             {
27                 mindis = dis[j];
28                 min = j;
29             }
30         vis[min] = 1;
31         for(int j=1; j<=n; j++)
32             if(mindis+Map[min][j]<dis[j] && Map[min][j]!=INF && vis[j]==0)
33                 dis[j] = mindis+Map[min][j];
34     }
35 }
36 
37 
38 int main()
39 {
40 //    freopen("in.txt", "r", stdin);
41     return 0;
42 }

 

其实上述dij在实际竞赛中时不常用的,因为他的复杂度太高,不能符合比赛中大多数题目对于时间效率的要求。我们实际使用的dij使用优先队列优化的Dij时间复杂度为O(mlogn)。仔细想想我们会发现在最坏情况下也就是对于一个完全图m=n(n-1)那么这个版本的的dij复杂度不是退化成O(n^2logn)不仅较以前没有降低反而上升了,这还叫优化?等等,这只是理论上分析而已。在实际使用中由于他的入队条件往往得不到满足,所以实际的使用效率会大大的好于O(n^2)的版本,所以你大可放心使用。

下面来讲述一下到底是怎么对原来的算法进行优化的?前面我分了三块讲述的dij我们可以很清晰的看见影响复杂度的是第三块,第三块又分为两部分--

(1)找出dis最小的点

对于这一块直接使用优先队列就可以了,对于每次选出最小值的操作只需要logn的时间复杂度。

(2)对其连接的所有边进行松弛

对于这一块也很容易我们可以不用邻接矩阵而用邻接表存储。这样很容易证明当左右顶点都访问过后正好每一条边都被松弛了一次。

综上所述复杂度O(mlogn)由此产生。

下面我们给出代码:

 

 1 /****************************************
 2      Dijkstra O(mlogn) 单元最短路算法        
 3      By 小豪                
 4 *****************************************/
 5 #include <iostream>
 6 #include <cstdio>
 7 #include <cstring>
 8 #include <cstdlib>
 9 #include <algorithm>
10 #include <utility>
11 #include <vector>
12 #include <queue>
13 #include <stack>
14 #define INF 500001
15 #define LEN 50100
16 using namespace std;
17 
18 typedef pair<int, int> pii;
19 vector<pii> Map[LEN];
20 int dis[LEN];
21 
22 void init(){for(int i=0; i<LEN; i++)Map[i].clear();}
23 
24 void Dijkstra(int vex){
25     priority_queue<pii, vector<pii>, greater<pii> > q;
26     int vis[LEN] = {0};
27     for(int i=0; i<LEN; i++) dis[i] = (i==vex?0:INF);
28     q.push(make_pair(dis[vex], vex));
29     while(!q.empty()){
30         pii nv = q.top(); q.pop();
31         int x = nv.second;
32         if(vis[x]) continue;
33         vis[x] = 1;
34         for(vector<pii>::iterator it = Map[x].begin(); it!=Map[x].end(); ++it){
35             int y = it->first, v = it->second;
36             if(dis[y]>dis[x]+v){
37                 dis[y] = dis[x]+v;
38                 q.push(make_pair(dis[y], y));
39             }
40         }
41     }
42 }
43 
44 int main()
45 {
46 //    freopen("in.txt", "r", stdin);
47     return 0;
48 }

 

关于代码的说明:

这里我习惯用vector<pair<int,int> >来存储图。pair的第一个值表示指向的结点,第二个值表示边的权值。

初始化操作和参数返回值和原来没有区别,只是后来改成类似于BFS的形式每次取出一个节点,如果该节点已经被访问过则丢弃,否则松弛所有该结点连接的边,再把松弛好的dis,与结点入队(新更新的值可以用来更新其他的结点)。直到队列为空算法结束。

二、Floyd算法

 

对于floyd算法比较简单,也比较实用,它的特点就是代码特别短。在比赛的时候背出来就可以了。

这里先给出我的代码:

 1 /****************************************
 2      Floyd O(n^3) 最短路算法        
 3      By 小豪                
 4 *****************************************/
 5 #include <iostream>
 6 #include <cstdio>
 7 #include <cstring>
 8 #include <cstdlib>
 9 #include <cmath>
10 #include <algorithm>
11 #define LEN 1010
12 #define INF 500001
13 using namespace std;
14 
15 int Map[LEN][LEN], dis[LEN][LEN];
16 int n, m;
17 
18 void init()
19 {
20     for(int i=0; i<LEN; i++){
21         for(int j=0; j<LEN; j++){
22             Map[i][j] = INF;
23             if(i==j)Map[i][j] = 0;
24         }
25     }
26 }
27 
28 void floyd()
29 {
30     for(int i=1; i<=n; i++){
31         for(int j=1; j<=n; j++){
32             dis[i][j] = Map[i][j];
33         }
34     }
35     for(int k=1; k<=n; k++){
36         for(int i=1; i<=n; i++){
37             for(int j=1; j<=n; j++){
38                 dis[i][j] = min(dis[i][j], dis[i][k]+dis[k][j]);
39             }
40         }
41     }
42 }
43 
44 int main()
45 {
46 //    freopen("in.txt", "r", stdin);
47     return 0;
48 }

算法的主体部分是三层for循环k表示i-j经过前k个结点所获得的最短路径,每一次比较原来的dis[i][j]是不是比经过k结点也就是dis[i][k]+dis[k][j]大,若果是则更新。

floyd算法的主题思想是动态规划。在实际运用中我们常常可以改变dis[i][j]状态的含义来计算出题目所需要的东西。这一类floyd变形的题目还是很常见,当然在状态记录信息不足时我们还可以增加一维用于记录其他信息(这是解动态规划题常用的方法),这里我就不再详细叙述了。

三、bellman-ford与SPFA算法(带负权的最短路问题)

在图论问题中我们还会遇到带负权图的单源最短路问题,这是dij算法就没有用武之地了,然而floyd算法的复杂度有过高(也有一些大材小用)。这是我们就需要用到下面两个算法:

1.bellman-ford算法

bellman-ford算法的不仅思想很简单,写起来也很简单,就两重循环对所有边松弛n次:

if(dis[y]>dis[x]+w[j])dis[y] = dis[x]+w[j];

 

这样在没有负权环的情况下我们可以求出图中的最短路。说明:算法中我们只存图的边即可。

代码如下:(部分代码)

 

1 for(int i=0; i<n-1; i++){
2     for(int j=0; j<m; j++){
3         int x = u[j], y = v[j];
4         if(dis[y]>dis[x]+w[j])dis[y] = dis[x]+w[j];
5     }
6 }    

 

 

 

虽然写起来简单,但是算法复杂度实在太高了。而实际使用中我们推荐使用更加优秀的SPFA算法。

2.SPFA算法

SPFA算法是西南交通大学段凡丁于1994年发表的。算法也十分容易实现,而且效率很不错。所以有很大的实用性,SPFA算法的思想是从广度优先搜索演变而来的。我们知道在对于一个不带权图上求最短路的时候我们常常会用用到BFS算法借助于队列先进先出的性质,先到达的结点所经历的步数一定是最短路。由于每个结点只入队一次,复杂度为O(n)。那么借助于这个思想是不是可以对于带权图也求出最短路呢。

SPFA就完成了这一点对于每次出队的节点对于所有这个结点连接的边执行松弛操作,若是dis数组被更新了则将结点重新入队。(因为更新好的结点有可能会影响到其他结点的最短路)这里就可以看出来每个结点可能多次入队,这里用k表示平均入队次数,所以复杂度为O(kn)在实际使用中k在2左右。可见spfa性能很卓越。

下面给出我的代码:

 

 1 /****************************************
 2      SPFA O(kn) 单源最短路算法        
 3      By 小豪                
 4 *****************************************/
 5 #include <iostream>
 6 #include <cstdio>
 7 #include <cstring>
 8 #include <cstdlib>
 9 #include <algorithm>
10 #include <queue>
11 #include <stack>
12 #include <vector>
13 #define LEN 1010
14 #define INF 0x3f3f3f3f
15 #define pb(a) push_back(a)
16 #define mp(a, b) make_pair(a, b)
17 
18 using namespace std;
19 
20 typedef pair<int, int> pii;
21 int n, dis[LEN];
22 vector<pii> Map[LEN];
23 
24 //返回false有负环true最短路在dis数组中
25 bool SPFA(int s)
26 {
27     queue<int> q;
28     int vis[LEN] = {0}, cnt[LEN] = {0};
29     for(int i=0 ;i<n; i++)dis[i] = INF;
30     dis[s] = 0;
31     q.push(s);
32     vis[s] = 1;
33     cnt[s]++;
34     while(!q.empty()){
35         int nv = q.front(); q.pop();
36         for(int i=0; i<Map[nv].size(); i++){
37             int x = Map[nv][i].first, y = Map[nv][i].second;
38             if(dis[x] > dis[nv]+y){
39                 dis[x] = dis[nv] + y;
40                 if(!vis[x]){
41                     q.push(x);
42                     vis[x] = 1;
43                     cnt[x] ++;
44                     if(cnt[x]>n) return false;
45                 }
46             }
47         }
48         vis[nv] = 0;
49     }
50     return true;
51 }
52 
53 int main()
54 {
55 //    freopen("in.txt", "r", stdin);
56     return 0;
57 }

 

对于代码的说明:

代码使用的存储结构与前面dij使用的邻接表是一样的,这里就不再讲一遍了。我们会发现这里多出来一个cnt数组。这个数组是干什么用的呢?

前面说过SPFA算法计算最短路图中是不能有负环的,那么如何判断负环,这里cnt数组就产生了作用,cnt是用来记录结点入队次数,可以证明当结点入队超过n次时说明图中存在负环。也有一些题目会要求你判断图中存不存在负环,这时候就可以使用SPFA算法。另一点和BFS区别就是在对于一个节点操作完后,我们需要去除结点标记。因为SPFA不像BFS每个结点只入队一次,而是需要多次入队,所以vis数组是用来标记当前节点是否存在队列中。

 

posted @ 2013-12-29 01:04  张小豪  阅读(2282)  评论(0编辑  收藏  举报