目录
使用区别:
所有边权都是正数的单源最短路:朴素Dijkstra算法,堆优化的Dijstra算法。
存在负权边的单源最短路:Bellman-ford,spfa。
多源汇最短路:floyd算法。
总结:
朴素Dijkstra算法: 适用于稠密图,适合邻接矩阵存储,时间复杂是 O(n^2+m), n表示点数,m 表示边数。
堆优化的Dijkstra算法:适用于稀疏图,适合邻接表存储,时间复杂度是O(mlogn)
Bell-Ford算法: 适用于处理可能存在负环的有限路线单源最短路问题,时间复杂度是O(nm),一般不能有负权回路。
spfa算法:时间复杂度是O(m)最坏O(nm)。
floyd算法:时间复杂度O(n^3)。
一.朴素Dijkstra算法
简介:迪杰斯特拉算法(Dijkstra)是由荷兰计算机科学家狄克斯特拉于1959年提出的,因此又叫狄克斯特拉算法。是从一个顶点到其余各顶点的最短路径算法,解决的是有权图中最短路径问题。迪杰斯特拉算法主要特点是从起始点开始,采用贪心算法的策略,每次遍历到始点距离最近且未访问过的顶点的邻接节点,直到扩展到终点为止。
算法思路:
按路径长度递增次序产生算法:
把顶点集合V分成两组:
(1)S:已求出的顶点的集合(初始时只含有源点V0)
(2)V-S=T:尚未确定的顶点集合
将T中顶点按递增的次序加入到S中,保证:
(1)从源点V0到S中其他各顶点的长度都不大于从V0到T中任何顶点的最短路径长度
(2)每个顶点对应一个距离值
S中顶点:从V0到此顶点的长度
T中顶点:从V0到此顶点的只包括S中顶点作中间顶点的最短路径长度
依据:可以证明V0到T中顶点Vk的,或是从V0到Vk的直接路径的权值;或是从V0经S中顶点到Vk的路径权值之和。
算法步骤如下:
G={V,E}
1. 初始时令 S={V0},T=V-S={其余顶点},T中顶点对应的距离值
若存在,d(V0,Vi)为弧上的权值
若不存在,d(V0,Vi)为∞
2. 从T中选取一个与S中顶点有关联边且权值最小的顶点W,加入到S中
3. 对其余T中顶点的距离值进行修改:若加进W作中间顶点,从V0到Vi的距离值缩短,则修改此距离值
简单地说就是先找出一个距离起点最近的,然后再由这个距离起点最近的点更新别的点
题目:https://www.acwing.com/problem/content/851/
代码如下:
#include<bits/stdc++.h>
using namespace std;
//dijkstra算法适用于正权边稠密图
const int N = 510;
int n,m;
int dist[N],g[N][N];//存放距离和图
bool st[N];//存放状态
int dijkstra()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
//遍历n次,将n个点的最值都找出
for(int i=0;i<n-1;i++)
{
int t = -1;//点
//从不确定的点中找出距离一号点最短的点
for(int j=1;j<=n;j++)
{
if(!st[j]&&(t==-1||dist[t]>dist[j]))
t = j;//依次比较距离1号点最近的点赋给t
}
//根据t更新t的出边
for(int j = 1;j<=n;j++)
{
dist[j] = min(dist[j],dist[t]+g[t][j]);
}
//表示这个点已经确定了
st[t] = true;
}
//如果没有边就是无穷
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
cin>>n>>m;
memset(g,0x3f,sizeof g);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b] = min(g[a][b],c);//因为有重边,取一条最短的边
}
int t = dijkstra();
cout<<t<<" ";
return 0;
}
二.堆优化的Dijkstra
简介:在确定一个还未确定最短距离的点中距离源点最近距离的点时 朴素Dijkstra 的方法是遍历所有的点通过比较找出最近的点,在这个地方可以使用 优先队列(小根堆) 来进行优化,在所有的点中取出距离最小的点。
算法思路:
首先,将 优先队列 定义成 小根堆,将源点的距离初始化为 0 加入到优先队列中。
然后,从这个点开始扩展。先将队列中的队头元素 ver 保存到一个临时变量中,并将队头元素出队,然后遍历这个点的所有出边所到达的点 j,更新所有到达的点距离源点最近的距离。
如果源点直接到 j 点的距离比源点先到 ver 点再从 ver 点到 j 点的距离大,那么就更新 dist[j],使 dist[j] 到源点的距离最短,并将该点到源点的距离以及该点的编号作为一个 pair 加入到优先队列中,然后将其标记,表示该点已经确定最短距离。因为是小根堆,所以会根据距离进行排序,距离最短的点总是位于队头。一直扩展下去,直到队列为空。
因为有 重边 的缘故,所以该点可能会有冗余数据,即如果在扩展的时候,第一次遍历到的点是 2 号点,距离 源点 的距离为 10,此时 dist[2] = 0x3f3f3f3f > dist[1] + distance[1 -> 2] = 0 + 10 = 10 所以 dist[2] 会被更新为 10,此时会将 {10, 2} 入队。但是很不巧从 源点 到 2 号点有一个距离为 6 的重边,当遍历到这个重边时,由于 dist[2] = 10 > dist[1] + distance[1 -> 2] = 0 + 6 = 6,所以 {6, 2} 也入队了,入队之后由于是 小根堆 所以 {6, 2} 会排在 {10, 2} 前面,所以 {6, 2} 会先出队,出队之后会被标记。所以当下一次再遇到已经被标记的 2 号点时,直接 continue 忽略掉冗余数据继续扩展下一个点即可。
题目:https://www.acwing.com/problem/content/852/
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+10;
int h[N],e[N],ne[N],w[N],idx;//稀疏图采用邻接表存储
typedef pair<int,int> p;//存结点距离和编号
int dist[N];
int n,m;
bool st[N];
//使用邻接表存储
void add(int a,int b,int c)
{
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a]=idx++;
}
int dijkstra()
{
priority_queue<p,vector<p>,greater<p> > q;
memset(dist,0x3f,sizeof dist);
q.push({0,1});
dist[1] = 0;
while(!q.empty())
{
p t = q.top();
q.pop();
int ver = t.second,dis = t.first;
//如果此节点已经确定最小值就continue
if(st[ver]) continue;
st[ver] = true;
//else就拓展此节点,根据t更新t的拓展节点
for(int i = h[ver];i!=-1;i=ne[i])
{
int j = e[i];
if(dist[j]>dist[ver]+w[i])
{
dist[j] = dist[ver]+w[i];
q.push({dist[j],j});
}
}
}
if(dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
cout<<dijkstra()<<" ";
return 0;
}
三.bellman_ford算法
简介:贝尔曼-福特算法(Bellman-Ford)是由理查德·贝尔曼和莱斯特·福特创立的,求解单源最短路径问题的一种算法。它的原理是对图进行V-1次松弛操作,得到所有可能的最短路径。其优于Dijkstra算法的方面是边的权值可以为负数、实现简单,缺点是时间复杂度过高,最坏O(nm).
Bellman-Ford算法是一种处理存在负权边的单元最短路问题的算法。解决了Dijkstra无法求的存在负权边的问题。 虽然其算法效率不高,但是也有其特别的用处。其实现方式是通过m次迭代求出从源点到终点不超过m条边构成的最短路的路径。一般情况下要求途中不存在负环。但是在边数有限制的情况下允许存在负环。因此Bellman-Ford算法是可以用来判断负环的。很重要的一点是本算法每次迭代都是在上一次的基础上进行的,因此我们在代码实现时要注意保留上一次的结果,在上一次的基础上算,故需要将上次的数组copy一份。
模板题目:https://www.acwing.com/problem/content/855/
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 510,M = 10010;
int dist[N],backup[N];
int n,m,k;
struct Edge{
int a,b,c;
}edges[M];
void bellman_ford()
{
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
//k表示从i到j经过k条边
for(int i=0;i<k;i++)
{
memcpy(backup,dist,sizeof dist);
for(int j=0;j<m;j++)
{
int a = edges[j].a , b = edges[j].b , c = edges[j].c;
//当dist[b]有值时,表示以b为尾的这条边更新完了
dist[b] = min(dist[b],backup[a]+c);
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=0;i<m;i++)
{
int a,b,c;
cin>>a>>b>>c;
edges[i] = {a,b,c};
}
bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");
else printf("%d\n", dist[n]);
return 0;
}
四.spfa算法
简介: SPFA 算法是Bellman-Ford算法的队列优化算法的别称,通常用于求含负权边的单源最短路径,以及判负权环。SPFA 最坏情况下复杂度和朴素Bellman-Ford相同,为O(nm)
优化思路:
Bellman-Ford算法在每次更新时,遍历了所有的边,而也如前面看到的,每次遍历未必会真的更新最短距离。SPFA算法就是对该步骤的优化。每次只有当前的起点到源点的距离变小了,该点连通的其他点才会变小。
只要一个结点的边变小了,我们就把他放进队列(其他的也行)中,只要队列中还有值,我们就继续更新,更新到的点也加入队列,如此往复,直到标记全部的点。
题目:https://www.acwing.com/problem/content/853/
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 100010;
int h[N],e[N],w[N],ne[N],idx;
int dist[N];
bool st[N];
int n,m;
void add(int a,int b,int c)
{
e[idx] = b,w[idx] = c,ne[idx] = h[a],h[a] = idx++;
}
int spfa()
{
queue<int> q;
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
q.push(1);
st[1] = true;
while(!q.empty())
{
int t = q.front();
q.pop();
st[t] = false;
for(int i=h[t];i!=-1;i=ne[i])
{
int j = e[i];
if(dist[j]>dist[t]+w[i])
{
dist[j] = dist[t]+w[i];
if(!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
int main()
{
cin>>n>>m;
memset(h,-1,sizeof h);
while(m--)
{
int a,b,c;
cin>>a>>b>>c;
add(a,b,c);
}
int t = spfa();
if(t==0x3f3f3f3f) cout<<"impossible"<<endl;
else cout<<t<<endl;
return 0;
}
五.floyd算法
简介:弗洛伊德算法是解决任意两点间的最短路径的一种算法,可以正确处理有向图或负权(但不可存在负权回路)的最短路径问题,同时也被用于计算有向图的传递闭包,Floyd 算法是一个基于贪心、动态规划求一个图中 所有点到所有点 最短路径的算法。
算法思路:
以每个点为「中转站」,刷新所有「入度」和「出度」的距离。每次从「未求出最短路径」的点中 取出 最短路径的点,并通过这个点为「中转站」刷新剩下「未求出最短路径」的距离。简单地说就是dist[i,j]表示从i到j经过k这个点的最短距离是多少。
题目:https://www.acwing.com/problem/content/853/
代码如下:
#include<bits/stdc++.h>
using namespace std;
const int N = 210,M = 20010;
int dist[N][M];
int n,m,k;
void floyd()
{
//以k这个点作为中转点更新从i到j的距离
for(int k=1;k<=n;k++)
{
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
//比较从i直接到j的距离和从i到j以k为中转的距离最小值
dist[i][j] = min(dist[i][j],dist[i][k]+dist[k][j]);
}
}
}
int main()
{
cin>>n>>m>>k;
for(int i=1;i<=n;i++)
for(int j=1;j<=m;j++)
{
if(i==j) dist[i][j]=0;
else dist[i][j] = 0x3f3f3f3f;
}
while(m--)
{
int x,y,z;
cin>>x>>y>>z;
dist[x][y] = min(dist[x][y],z);
}
floyd();
while(k--)
{
int x,y;
cin>>x>>y;
if(dist[x][y]>0x3f3f3f3f/2) cout<<"impossible"<<endl;
else cout<<dist[x][y]<<endl;
}
return 0;
}