正权图单源最短路(SSSP)- Dijkstra算法

引言:

DAG上的最短路可以用dp解得,容易得到状态转移方程。但如果图中可以有环,就要使用其他算法了。这里先考虑边权均为正的最短路问题。

 

Contents:

一、 普通的dijkatra实现  O(n2)

二、采用邻接表

三、 基于优先队列的Dijkstra O(mlogn)

 给出样例问题(图片引自Tanky Woo):

对下图中的有向图,用Dijkstra算法计算从任意源顶点到其它顶点间最短路径。

《正权图单源最短路(SSSP)- Dijkstra算法》

输入:

6
7
1 2 10
1 4 30
1 5 100
2 3 50
3 5 10
4 3 20
4 5 60
1
2
3
4
5
输出:
No.1->No.5 :60
No.2->No.5 :60
No.3->No.5 :10
No.4->No.5 :30
No.5->No.5 :0

接下来就此问题,分别介绍几种复杂度不同的实现方法。

一、 普通的dijkatra实现

首先来谈谈这个算法。记st为起点,dist[i]为结点 i 到st的最短路径长。

初始化dist[st] = 0; 其余dist为INF(类似maxint)。当前结点 u = st。

从当前点u出发遍历邻点,更新dist[v] = min{ dist[v], dist[u]+w[u][v]};  ———-这里有点像状态转移

设一个集合S,总是把当前不在S中的结点的最小dist对应的点加入S中。

当所有点都在集合S中时,完毕。此时dist数组更新完毕,即从st到图上任意结点的最短路长都已经算出。

 

理解:关键在于集合S,每次取当前最小的dist。可见取到S中的点的dist都是已经更新完毕的了,即已算出的。

可以用反证明法想想,如果某次dist[i]是最小的dist,但是st到 i 还存在经过 u’ 的更短的路,则

dist[u’]必然 小于dist[i],即u’会比 i 先成为当前点,那时dist[i]就会在u’作为当期点的时候被更新为

dist[u’]+w[u’][i]了。 所以,dist[i]必是st到 i 的最短路长。

 

每次取一个元素到S中,所以循环n-1次,每次循环时,求最小dist(遍历n个结点)和更新相邻dist值(若采用邻接矩阵)都是O(n)的,所以时间复杂度为O(n^2)。

 

这里附上其他说法,本质一样,链接如下:

http://www.nocow.cn/index.php/Dijkstra

http://www.wutianqi.com/?p=1890

http://baike.baidu.com/view/7839.htm

 

下面给出详细注释的实现代码:

#include<cstdio> #include<cstring> using namespace std; #define MAXN 100 //最大结点数 #define INF 1<<30 int G[MAXN][MAXN]; int w[MAXN][MAXN]; //每条边权值 int vis[MAXN]; //将已经在集合S中的结点标记为 1 int dist[MAXN]; //记录 st点 到所有点的最短路径长 int slen; //集合S现有结点数 int n; //结点数 void dijkstra(int st) { for(int i=0; i<n; i++) dist[i] = (i==st ? 0 : INF); //初始 //或者 for(int i=0; i<n; i++) dist[i] = INF; dist[st] = 0; vis[st] = 1; //把 st点放进S集合 int u = st; //初始u while(slen != n)//也可直接循环n-1次 { //从u点出发,更新dist //如果扩展的i已经在集合S中,则不可能被更新,所以加上!vis[i] for(int i=0; i<n; i++) if(!vis[i] && G[u][i]) dist[i] = dist[i] < dist[u]+w[u][i]? dist[i] : dist[u]+w[u][i]; //printf(“%8d%8d%8d%8d/n”, dist[2], dist[3],dist[4],dist[5]); int min = INF; //找不在S中,且dist最小的点 for(int i=0; i<n; i++) if(!vis[i]) min = min<=dist[i]? min: (u = i, dist[i]); //遍历结点 0~n-1 vis[u] = 1; //或者 min = min<=dist[i]? min: dist[u = i]; slen++; } } void read_graph() { int e; //边数 int u, v; memset(G,0,sizeof(G)); //其实全局本来就初始为0 ..(?) scanf(“%d%d”, &n, &e); for(int i=0; i<e; i++) //输入e条边 { scanf(“%d%d”,&u, &v); scanf(“%d”, &w[u][v]); //u->v 这里考虑有向图 G[u][v] = 1; } } int main() { read_graph(); int st; while(scanf(“%d”, &st) !=EOF) //e.g.求任意起点到最后一个结点 { slen = 1; memset(vis, 0, sizeof(vis)); dijkstra(st); printf(“No.%d->No.%d :%d/n”, st, n-1, dist[n-1]); } }

注:对于上面的G和w数组,可以省掉一个G,只要在w初始化时初始为INF,就可以隐式地表示边不存在。

也不需要判断相邻了。

 

 二、采用邻接表

 注意到上面找相邻点时都要循环n次,故这里采用邻接表优化。

同样也可以用verctor<int> G[MAXN];

代码① verctor<int> G[MAXN]  :

 #include<cstdio> #include<cstring> #include<vector> using namespace std; #define MAXN 100 #define INF 1<<30 //int G[MAXN][MAXN]; //邻接矩阵 vector<int> G[MAXN]; //跟邻接表不同,但类似 int w[MAXN][MAXN]; int vis[MAXN]; int dist[MAXN]; int n; void dijkstra(int st) { for(int i=0; i<n; i++) dist[i] = (i==st ? 0 : INF); vis[st] = 1; int u = st; for(int i=0; i<n-1; i++) { //for(int i=0; i<n; i++) if(i!=u && G[u][i]) dist[i] = dist[u]+w[u][i]; for(int i=0; i<(int)G[u].size(); i++) dist[G[u][i]] = dist[G[u][i]] < dist[u]+w[u][G[u][i]]? dist[G[u][i]] : dist[u]+w[u][G[u][i]]; //!! int min = INF; for(int i=0; i<n; i++) if(!vis[i]) min = min<=dist[i]? min: dist[u = i]; if(min == INF) break; //说明其他点是无法到达了 vis[u] = 1; } } void read_graph() { int e; int u, v; scanf(“%d%d”, &n, &e); for(int i=0; i<e; i++) { scanf(“%d%d”,&u, &v); scanf(“%d”, &w[u][v]); //G[u][v] = 1; G[u].push_back(v); } } int main() { read_graph(); int st; while(scanf(“%d”, &st) !=EOF) { memset(vis, 0, sizeof(vis)); dijkstra(st); printf(“No.%d->No.%d :%d/n”, st, n-1, dist[n-1]); } }

代码② 数组实现邻接表(+二叉堆,具体下面会解释)(by Rujia Liu):

 #include<cstdio> #include<cstring> #include<queue> using namespace std; const int INF = 1000000000; const int MAXN = 1000; const int MAXM = 100000; int n, m; int first[MAXN], d[MAXN]; int u[MAXM], v[MAXM], w[MAXM], next[MAXM]; typedef pair<int,int> pii; int main() { scanf(“%d%d”, &n, &m); for(int i = 0; i < n; i++) first[i] = -1; for(int e = 0; e < m; e++) { scanf(“%d%d%d”, &u[e], &v[e], &w[e]); next[e] = first[u[e]]; first[u[e]] = e; } priority_queue<pii, vector<pii>, greater<pii> > q; bool done[MAXN]; for(int i = 0; i < n; i++) d[i] = (i==0 ? 0 : INF); memset(done, 0, sizeof(done)); q.push(make_pair(d[0], 0)); while(!q.empty()) { pii u = q.top(); q.pop(); int x = u.second; if(done[x]) continue; done[x] = true; for(int e = first[x]; e != -1; e = next[e]) if(d[v[e]] > d[x]+w[e]) { d[v[e]] = d[x] + w[e]; q.push(make_pair(d[v[e]], v[e])); } } for(int i = 0; i < n; i++) printf(“%d/n”, d[i]); return 0; }

 

三、 基于优先队列的Dijkstra

上面说到二叉堆, 其实就是使用二叉堆(Binary Heap)来保存没有扩展过的点的距离并维护其最小值,

并在访问每条边的时候更新。O(mlogn)。

而这里用的是优先队列,其实实质差不多,效果一样。上面的Lrj那个用的其实也是优先队列。

step1

为了优化“找出不在S中的最小dist”,采用优先队列(priority_queue),在优先队列中,元素是按优先级排列的,

所以不再是front,而是top,pop删除的也是优先级最高的元素。 而它默认是利用元素自身“小于”操作符来定义优先级的,

所以默认是不断pop最大的数。在这里,要用great<int>表示大于运算符,或者重载元素的小于操作,来使得出队的总是最小的dist。

step2

还有一个问题,就是我们要得到的是最小dist的结点标号,所以这里用到STL的pair,把dist[i]跟 i 捆绑在一起入队,因为pair定义是

先比较第一维,相当才比较第二维,所以组合方式是(dist[i], i) 而不是 (i, dist[i])。

 

给出代码,可以看成是代码(二①)的优化:

#include<cstdio> #include<cstring> #include<vector> #include<queue> //priority_queue using namespace std; #define MAXN 100 #define INF 1<<30 vector<int> G[MAXN]; //跟邻接表不同,但类似 typedef pair<int, int> pii; //捆绑 (dist[i], i) priority_queue<pii, vector<pii>, greater<pii> > q; //!!注意priority_queue 的定义, greater大于运算符 //至此 q 为一个(越小优先级越高)的优先队列 int w[MAXN][MAXN]; int vis[MAXN]; int dist[MAXN]; int n; void clear() { while(!q.empty()) q.pop(); } void dijkstra(int st) { for(int i=0; i<n; i++) dist[i] = (i==st ? 0 : INF); vis[st] = 1; //已经在集合S中 int u = st; for(int i=0; i<n-1; i++) //事实上,有向图不一定能到达所有点,也就是说不一定所有点都能到 { //也就是不一定所有点都能到S集合中(至于dist为INF的点,就是无法到达了) for(int i=0; i<(int)G[u].size(); i++) { int v = G[u][i]; if(vis[v]) continue; //已经在S中,不可能被更新,避免重复扩展 dist[v] = dist[v] < dist[u]+w[u][v]? dist[u]: dist[u]+w[u][v]; //称为松弛操作 q.push(make_pair(dist[v], v)); //还不在集合S中的,入队 } //int min = INF; if(q.empty()) break; //没有可供选择的dist pii t = q.top(); q.pop(); //u为最小dist的pii u = t.second; //!! t.second //for(int i=0; i<n; i++) if(!vis[i]) min = min<=dist[i]? min: dist[u = i]; vis[u] = 1; } //printf(“q size:%d/n”, q.size()); void clear_queue(); //人为把queue里多余的dist清除,方便下一个源点 } void read_graph() { int e; int u, v; scanf(“%d%d”, &n, &e); for(int i=0; i<e; i++) { scanf(“%d%d”,&u, &v); scanf(“%d”, &w[u][v]); G[u].push_back(v); } } int main() { read_graph(); //G不用清除,因为本测试样例只针对同一个图 int st; while(scanf(“%d”, &st) !=EOF) { memset(vis, 0, sizeof(vis)); dijkstra(st); printf(“No.%d->No.%d :%d/n”, st, n-1, dist[n-1]); } }

 

 

    原文作者:Dijkstra算法
    原文地址: https://blog.csdn.net/tclh123/article/details/6303524
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞