康复计划之最短路
最短路问题:给你一张图(n个点,m条边),每条边有一个距离。问从一个点到另一个点的最短距离。
最短路主要关注两种算法: Dijkstra O(n^2) 和 SPFA O(n*m) (都是最坏复杂度) (其实SPFA几乎无法达到最坏复杂度)
Dijkstra:
适用范围(局限性):不存在负权边
思路:
我们取两个集合,一个集合M中为已确定最短距离的点集,另一个集合N为还没有确定最短距离的点集。
我们用集合M中的所有点去更新N中的点的距离,那么集合N中距离最小的点就可以确定该距离为最小距离了。
最初集合M中只有出发点,逐渐向外扩展。
对于思路中的第二行,我们予以证明:
假设用集合M中的点更新N中的点的距离后,N中的最小距离点o不能被确定为最短路径,那么N中必须存在一个点p,使得M->p->o为o点最短路径,即s(M->p->o) < s(M->o)。而s(M->p) > s(M->o) 且 s(p->o) > 0(不存在负权边),所以s(M->p->o) < s(M->o)不成立。推出矛盾。
故第二行得证。
然后我们考虑代码实现。
最容易想到的做法为每往集合M中加入一个元素x,更新由x可扩展到的点的最短距离。然后遍历一遍所有点,找到位于集合N中且距离最小的点,把它加入集合M中...
如此循环。确定复杂度O(n^2)
我们考虑一个可以优化的部分:遍历一遍所有点,找到位于集合N中且距离最小的点,把它加入集合M中。
我们只需要找到一个点,但我们遍历了所有点,这是不是没有必要呢?
我们可以用一个小根堆,去储存集合N中的点的距离,那么我们每次寻找最小距离的点的复杂度就可以降低到logn。
于是Dijkstra的复杂度就降低到了O(m+nlogn)
(其实未优化Dijkstra的复杂度为O(m+n^2),由于 m <= n^2,故写为O(n^2) )
给个模板题:click here
Dijkstra:
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<queue> 6 using namespace std ; 7 const int N = 100000 + 10 ; 8 const int M = 100000 + 10 ; 9 const int INF = 0x7ffffff ; 10 11 inline int read() { 12 int k = 0, f = 1 ; char c = getchar() ; 13 for( ; !isdigit(c) ; c = getchar()) 14 if(c == '-') f = -1 ; 15 for( ; isdigit(c) ; c = getchar()) 16 k = k*10 + c-'0' ; 17 return k*f ; 18 } 19 20 struct Edge { 21 int to, nex, val ; 22 }e[N] ; 23 int n, m, cnt = 0 ; int head[N], dis[N] ; 24 bool vis[N] ; 25 26 struct HeapNode { 27 int d, u ; 28 bool operator < (const HeapNode& rhs) const { 29 return d > rhs.d ; 30 } 31 }; 32 priority_queue<HeapNode>q ; 33 34 inline void add_edge(int x,int y,int z) { 35 e[++cnt].nex = head[x] ; head[x] = cnt ; e[cnt].to = y ; e[cnt].val = z ; 36 } 37 38 inline void djsk() { 39 dis[1] = 0 ; 40 for(int i=2;i<=n;i++) dis[i] = INF ; 41 memset(vis,0,sizeof(vis)) ; 42 q.push((HeapNode){0,1}) ; 43 while(!q.empty()) { 44 HeapNode xx = q.top() ; q.pop() ; 45 int x = xx.u ; 46 if(vis[x]) continue ; vis[x] = 1 ; 47 for(int i=head[x];i;i=e[i].nex) { 48 int y = e[i].to ; if(vis[y]) continue ; 49 if(dis[y] > dis[x]+e[i].val) { 50 dis[y] = dis[x] + e[i].val ; 51 q.push((HeapNode){dis[y],y}) ; 52 } 53 } 54 } 55 } 56 57 int main() { 58 n = read(), m = read() ; 59 for(int i=1;i<=m;i++) { 60 int x, y, z ; x = read(), y = read(), z = read() ; 61 add_edge(x,y,z) ; 62 } 63 djsk() ; 64 printf("%d\n",dis[n]) ; 65 return 0 ; 66 }
SPFA:
适用范围:不存在负权环 (可以存在负权边) (可以检验图中是否存在负权环)
其实SPFA是Bellman-Ford算法的优化,所以我们先介绍Bellman-Ford算法。
Bellman-Ford:
思路:
采用bfs的方法一层一层往外扩展。
每扩展一层,用该扩展层去更新更下面一层,并对于可更新的点,将其加入下一层要扩展的集合中。
第零层为出发点。
最坏复杂度:O(n*m) (其实难以达到)
SPFA:
用一个先进先出队列储存可更新的点。
每次从队首取出一个点x,用x去松弛其它点,遇到能松弛的点x,就将x加入队尾。
直至队列为空。
一个小优化:
如果dis[y] < dis[q.front()] 就将其加入队首,而不是队尾。
SPFA代码(双向队列优化):
1 #include<iostream> 2 #include<cstdio> 3 #include<cstring> 4 #include<cmath> 5 #include<queue> 6 using namespace std ; 7 const int N = 100000 + 10 ; 8 const int M = 100000 + 10 ; 9 const int INF = 0x7ffffff ; 10 11 inline int read() { 12 int k = 0, f = 1 ; char c = getchar() ; 13 for( ; !isdigit(c) ; c = getchar()) 14 if(c == '-') f = -1 ; 15 for( ; isdigit(c) ; c = getchar()) 16 k = k*10 + c-'0' ; 17 return k*f ; 18 } 19 20 struct Edge { 21 int to, nex, cos ; 22 }e[M] ; 23 24 int n, m ; int head[N], dis[N] ; 25 bool inq[N] ; 26 deque<int>q ; 27 28 inline void add_edge(int x,int y,int z) { 29 static int cnt = 0 ; 30 e[++cnt].to = y, e[cnt].nex = head[x], head[x] = cnt, e[cnt].cos = z ; 31 } 32 33 int main() { 34 int n, m ; 35 n = read(), m = read() ; 36 for(int i=1;i<=m;i++) { 37 int x = read(), y = read(), z = read() ; 38 add_edge(x,y,z) ; 39 } 40 for(int i=2;i<=n;i++) dis[i] = INF ; 41 q.push_back(1) ; dis[1] = 0 ; 42 while(!q.empty()) { 43 int x = q.front() ; q.pop_front() ; inq[x] = 0 ; 44 for(int i=head[x];i;i=e[i].nex) { 45 int y = e[i].to ; 46 if(dis[y] > dis[x]+e[i].cos) { 47 dis[y] = dis[x]+e[i].cos ; 48 if(!inq[y]) { 49 if(!q.empty() && dis[y] < dis[q.front()]) q.push_front(y) ; 50 else q.push_back(y) ; 51 inq[y] = 1 ; 52 } 53 } 54 } 55 } 56 printf("%d",dis[n]) ; 57 return 0 ; 58 }