最短路算法
前言
本文意在介绍四种常用的最短路算法,并对不同情况下算法的选择进行分析。
不同算法的区别,在于时间复杂度以及负边、负环等的处理。
至于空间复杂度,影响不大。
前置知识
松弛操作
所有算法的底层原理都是松弛操作。
寻求
若 dis[v]=min(dis[v],dis[k]+e[k][v]);
负环、正环
负边:权值是负数的边。反之为正边(边长一定是正边)。
对于一个环,若其权值和为负数,则称之为负环,为正数就叫正环。
最短路径中不能存在负环或正环:
-
遇到负环,会无限绕圈,值是无限小的。
-
遇到正环,显然去掉环的一些边后可得到更小值,换言之这些边可有可无,反正最后会回到起始边。
最短路径的最优子结构
令
对于任意边
反过来也成立。若上式成立,则
几种算法
首先,我们定义
Dijkstra
简介
单源最短路算法,时间复杂度
适用于正权边、稠密图。
将所有点分为:未确定最短路径的点集合
贪心的思路,每次选择最优点松弛其它点至源点的距离。正因如此,无法处理存在负边的情况。
算法流程
- 初始化
dis 为极大数\operatorname{INF} ,并将源点加入S 中。 - 从每次从
V 中选取到dis 值最小的点u ,将u 插入S ,并松弛其出边指向之点。 - 重复上一步
n-1 次。
若
堆优化
朴素做法的第二步耗费大量时间,考虑使用小根堆维护
- 队列元素为
\operatorname{pair} 类型,前项即为最短距离,后项为节点编号。 - 仿照朴素做法初始化,并把
u 加入堆。 - 取出堆顶元素,若已在
S 中则弹出;否则加入S ,并松弛其出边指向之点k_i 。如果发现dis[k_i] 松弛后更优,则将k_i 加入堆。 - 重复上一步直到堆为空。
可完全替代朴素做法,代码如下:
#define pir pair<long long,int>
void dj(int k){
priority_queue<pir,vector<pir>,greater<pir>>q; //优先队列
memset(dis,0x7f7f7f,sizeof(dis));
dis[k]=0; //初始化
MAX=dis[0]; //极大数
q.push({0,k}); //将源点加入队列
while(!q.empty()){
long long s=q.top().first;
int k=q.top().second;
q.pop();
if(vis[k]){ //弹出重复点
continue;
}
vis[k]=1;
for(int i=head[k]; i;i=e[i].nxt){
int j=e[i].v;
if(!vis[j]&&dis[k]+e[i].w<dis[j]){ //将更新后更优点加入队列
dis[j]=dis[k]+e[i].w;
q.push({dis[j],j});
}
}
}
}
时间复杂度是
Bellman-ford
简介
单源最短路算法,时间复杂度
可用于存在负边的情况,并能判断是否存在负环。
核心思想:对所有边松弛,操作
算法流程
- 遍历所有边,设每条边起点为
x_i ,终点为y_i ,使用dis[x_i] 松弛dis[y_i] 。 - 重复上一步
n-1 次。
判断负环:操作完
void Bf(int root){
memset(dis,0x3f,sizeof(dis));
MAX=dis[0];
dis[root]=0;
for(int i=1; i<=n-1;i++){
for(int j=1; j<=cnt;j++){ //用邻接表存储,边为 1~cnt。
dis[e[j].v]=min(dis[e[j].v],dis[e[j].u]+e[j].w);
}
}
}
正确性简要证明
打代码时,不免产生疑惑:为啥运行
设最短路径为
由于我们是遍历所有边,因此第
于是遍历
SPFA
简介
单源最短路算法,是
参数
算法流程
核心思想:只松驰可能有意义(更新后更优)的边。
- 将起点加入队列。
- 取出队头
k ,松弛其出边(k,v,w) 。若松弛后dis[v] 值更优,就将该v 加入队列。 - 统计每个点入队次数
d ,若d>n-1 ,就说明存在负环。 - 重复执行上两步直到队列为空。
判断负环
第三步的作用是判断负环,容易被卡,因此最好使用其他方法:
较常用的方法:设
或是统计源点至所有点(不包含终点)的最短路径所包含的边数
玄学方法:在快要超时时直接结束算法,并认为存在负环。一般来说所有点入队次数超过
其它优化
SLF:优先考虑权值更优的点。若新加入的数优于队头元素,则入队头,否则入队尾。双向队列
LLL:大权值点推迟处理。若队头元素的权值大于全队平均值,则插入队尾,不断操作直到遇见反例。
只用一种提速二三成,双管齐下可快出五成。
Floyd
简介
多源最短路算法,时间复杂度
算法流程
-
初始化数组,若存在边
(x,y,w) ,则令dis[x][y]=w 。 -
将路径用三点区分:起点
u 、中转点k 、终点v 。 -
依次枚举这三点,更新路径值:
dis[u][v]=min(dis[u][v],dis[u][k]+dis[k][v]);
代码如下:
void floyd(){
for(int k=1; k<=n;k++)
for(int i=1; i<=n;i++)
for(int j=1; j<=n;j++){
dis[i][j]=dis[i][k]+dis[k][j];
}
}
对比总结
能不用