目录
Dijkstra算法
适用范围:单源 无负权边
dijkstra算法是基于贪心的思想,每次选择至起点距离最近的点,去更新与其相邻的点,之后不再访问,不会回头。其因为不会回头这一性质,无法处理带负权边的图。
朴素版dijkstra
针对稠密图使用时,时间性能还不算很差
模板题:Acwing 849
int g[N][N];// 该图为稠密图 用邻接矩阵来存储
// 用于存储每个点到起点的最短距离
int dist[N];
// 记录是否找到了起点到该节点的最短距离
bool state[N];
int dijkstra(){
// 将距离初始化为最大值
memset(dist,0x3f,sizeof dist);
// 起点为1号点
dist[1] = 0;
for(int i = 0;i < n; i++){
// 取-1 处理每次循环第一次找到的临界情况
int t = -1;
// *1 寻找未确定状态下离起点 距离最小 的结点
// 存在优化空间-此处遍历了全部数来寻找最小值
for(int j = 1;j <= n; j++){
if(!state[j] && (t == -1 ||dist[t] > dist[j]) ){
t = j;
}
}
// *2 更新 这个结点的确定状态
state[t] = true;
// *3 更新 在该节点确定后的 各节点的最短路径
// 其实是只需遍历更新 与节点t相邻各节点 的最短路径
// 但是根据稠密图的性质 直接遍历所有节点更加方便
for(int j = 1; j <= n; j++){
dist[j] = min(dist[j],dist[t] + g[t][j]);
}
}
if(dist[n] == 0x3f3f3f3f)
return -1;
return dist[n];
}
为什么使用 0x3f3f3f3f 表示无穷大?
①.对于memset赋值操作方便,memset(dist,0x3f,sizeof dist) 按字节操作赋值为0x3f3f3f3f
②. 0x3f3f3f3f数量级达到了10^9,一般图的边权达不到9次方,认为其为无穷大没有问题
③.因为松弛操作经常需要先将两边的权值相加后再使用min来进行比较,若使用严格无穷大0x7fffffff,两个无穷大的边权相加会发生溢出。0x3f3f3f3f很好地解决了该问题
算法的主要耗时的步骤是 *1
即从dist 数组中选出 未确定状态下离起点 距离最小 的结点
只是找个最小值而已,没有必要每次遍历一遍dist数组。
在一组数中每次能很快的找到最小值,很容易想到使用小根堆,我们考虑使用STL的优先队列对该算法步骤进行优化, 可将该步骤时间复杂度 从O(n^2) 降为了 O(1)
堆优化版dijkstra
**最常用的!只要无负权边的图,皆优先使用该方法模板!(SPFA存不了过大的图,且可能会被卡)
模板题:Acwing 850
int h[N],e[N],ne[N],w[N],idx;
// 稀疏图使用邻接表存储
int dist[N];
// 是否找到了起点到该节点的最短距离
bool state[N];
int dijkstra(){
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
// 定义小根堆 求 未确定状态下到起点距离最小的节点
priority_queue<PII,vector<PII>,greater<PII> > heap;
// PII{至起点的距离,点编号}
// 因为优先队列默认比较first
heap.push({0,1});// 放入起点
while(heap.size()){
auto t = heap.top();
heap.pop();
int num = t.second;
// 因为有重边 所以找到最小边时要给点打标记
if(state[num]) continue;
state[num] = true;
// 遍历所有与num号点有相邻边的点
for(int i = h[num];i!=-1; i = ne[i]){
int j = e[i];//相邻点的序号
if(dist[j] > w[i] + t.first){
dist[j] = w[i] + t.first;
heap.push({dist[j],j});
}
}
}
return (dist[n] == 0x3f3f3f3f)?-1 :dist[n];
}
参考资料:AcWing 850. 朴素Dijkstra与堆优化Dijkstra总结 - AcWing
AcWing 849. Dijkstra求最短路 I:图解 详细代码(图解) - AcWing
bellman-ford算法
适用范围:单源 有负权边 判断负环 PS:效率低
bellman_ford算法无需对自环与重边进行额外处理(参考资料:Bellman_ford算法 - AcWing)
同时也可以存在负权回路,因为它求得的最短路是有限制的,是限制了边数的,这样不会永久的走下去,会得到一个解;
SPFA算法各方面优于该算法,但是在碰到限制了最短路径上边的长度时就只能用bellman_ford了,此时直接把n重循环改成k次循环即可(即以下模板题)
// 该算法无非就是循环n次然后遍历所有的边,因此不需要做什么特别的存储
// 只要把所有的边的信息存下来能够遍历即可
struct Edge{
int a,b,w;
}edges[M];
int bellman_ford(){
memset(dist,0x3f,sizeof dist);
dist[1] = 0;
// 根据限制 遍历k次
for(int i = 0;i < k; i++){
// 使用backup:避免给a更新后立马更新b, 这样b一次性最短路径就多了两条边出来
memcpy(backup,dist,sizeof dist);
// 遍历所有边,而朴素dijkstra是遍历所有顶点n*n
// spfa主要优化此步骤 是没有必要遍历所有边的
for(int j = 1;j <= m; j++){
int a = edges[j].a, b = edges[j].b, w = edges[j].w;
dist[b] = min(dist[b],backup[a] + w);
}
}
return dist[n];
}
练习题:lc 787. K 站中转内最便宜的航班
SPFA算法
shortest path faster algorithm 适用范围:单源 有负权边 判断负环
代码模板
模板题1:Acwing 851 (设起点为1,终点为n,求最短路代码)
注意:state数组的含义与dijkstra的不一样 需要辨析
int h[N],e[N],ne[N],w[N],idx;
bool state[N];
// 表示当前是否放入队列里的状态
int spfa(){
memset(dist,0x3f,sizeof dist);
// 定义起点为1
dist[1] = 0;
queue<int> q;q.push(1);
state[1] = true;
while(q.size()){
// 对队头进行松弛操作
int t = q.front();
q.pop();
// 队头出队后更新状态
state[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];
// state的作用:
// 如果此时该点已在队列中则无需重复入队 该点出队更新一次即可
if(!state[j]){//更新状态
q.push(j);
state[j] = true;
}
}
}
}
return dist[n];
}
模板题2:Acwing 852 spfa判断负环
与以上模板的差别在于:需要维护cnt数组 记录每个点至起点的路径边数,同时初始将图中所有点全部加入队列中(若不全部加入,只能判断从起点至终点的所有路径是否存在负环,存在遗漏)
int h[N],e[N],ne[N],w[N],idx;
int dist[N],cnt[N];
bool state[N];
bool spfa(){
queue<int> q;
for(int i = 1;i <= n; i++){
q.push(i);
state[i] = true;
}
while(q.size()){
int t = q.front();
q.pop();
state[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];
cnt[j] = cnt[t] + 1;
if(cnt[j] >= n)
return false;
if(!state[j]){
q.push(j);
state[j] = true;
}
}
}
}
return true;
}
参考资料:AcWing 851. spfa和bellman-ford的区别,以及和djikstra的区别 - AcWing
AcWing 851. spfa求最短路---图解--$\color{red}{海绵宝宝来喽}$ - AcWing
与以上算法的分析比较
与Dijsktra比较
dijkstra算法是基于贪心的思想,每次选择至起点距离最近的点,去更新与其相邻的点,之后不再访问,不会回头。其因为不会回头这一性质,无法处理带负权边的图。
所以优化版的dijkstra基于的是优先队列,小根堆保证了能以O(1)的代价得到距离最近的点,满足了贪心思想。
而spfa算法,只要有某个点的距离被更新了,就把它加到队列中,去更新其它点,每个点都有被重复加入队列的可能(即可以回头),所以可以解决负权边的问题。
与bellman-ford比较
Bellman-ford算法不管三七二十一,遍历n次/k次,每次遍历图中的所有边(m条边),所以时间复杂度为O(n*m)
遍历每条边的时候,进行松弛操作,把入度的点的距离更新成最小。
然而,这样就循环遍历了很多用不到的边。比如第一次遍历,只有第一个点的临边是有效的。
spfa算法主要就是对该过程进行优化得来的
模板练习题:
Acwing 1127 (堆优化迪杰与SPFA均可解决)
lc 743. 网络延迟时间 (以上两种方法均可)
Floyd算法
基本思想:递推产生一个dist[k][i][j],表示 i -> j的路径长度,k表示绕行第k个顶点的运算步骤。若i与j之间存在边,则以此边上的权值作为它们之间的最短路径长度;若不存在边,先用正无穷表示。
之后逐步尝试在原路径中加入顶点k作为中间顶点。若增加中间顶点k后路径长度更短,则更新dist
适用范围:多源最短路 有负权边 (ps 时间复杂度高 O(n^3) )
原理:基于动态规划思想 参考资料:AcWing 854. Floyd闫式dp分析法 - AcWing
因为k为最外层的遍历,所以可以将状态转移方程进行压缩,忽略k这一维度
dist[i][j] = min(dist[i][j],dist[i][k] + dist[k][j])
模板题:Acwing 854
// 稠密图使用邻接矩阵来求
// d[i][j]-点i至点j的最短路
int d[N][N];
// n为图的点数
void floyd(){
for(int k = 1; k <= n; k++){
for(int i = 1; i <= n; i++){
for(int j = 1; j <= n; j++){
d[i][j] = min(d[i][j],d[i][k]+d[k][j]);
}
}
}
}