【Acwing】最短路算法
0 前言
最短路算法包含以下五种,注意区分
1 Dijkstra算法
该算法使用于单源最短路、无负边权的图
单源最短路就是指,从一个指定点出发到指定终点 的最短路径
该算法有两种写法,第一种是朴素版本Dijkstra,复杂度o(n^2),适合稠密图
另一个版本是采用堆优化的
适用于稀疏图,边少的时候
因为朴素版本最复杂的一步是在找最小距离的点,于是我们将dist用堆来存储,在找最小距离的点的时候,直接输出堆顶元素就好了,此时查找的复杂度是O(1)的。
于是算法的复杂度受限于更新边,总的算法复杂度为O(mlogn)
1.1 朴素版
朴素版Dijkstra的思路就是两重遍历。参考这个视频的过程,
外层循环就是n次循环,每次从所有未标记的点中找到路径中的一个节点,当找完这n个点就找到了整个图的最短路径(相当于更新完毕n个点的dist数组)
内层循环则是 从当前所有未被标记的点中,选择距离最小的点。(按照视频中的,我们只需要处理出边的节点,但是这里直接遍历所有节点也能达到同样的效果,复杂度虽然没有那么完美,但是写法简单)
// dist[i]表示从起点到点i的最小距离之和,st[]是标记数组 int Dijkstra() { memset(dist, 0x3f,sizeof dist); //初始化距离 0x3f代表无限大 dist[1]=0; //第一个点到自身的距离为0 for(int i=0;i<n;i++) //有n个点所以要进行n次迭代 { int t=-1; //t存储当前访问的点,t=-1初始化一个不存在的点 for(int j=1;j<=n;j++) //这里的j代表的是从1号点开始,遍历所有未标记的点 if(!st[j]&&(t==-1||dist[t]>dist[j])) t=j; st[t]=true; for(int j=1;j<=n;j++) //依次更新每个点所到相邻的点路径值 dist[j]=min(dist[j],dist[t]+g[t][j]); } if(dist[n]==0x3f3f3f3f) return -1; //如果第n个点路径为无穷大即不存在最低路径 return dist[n]; }
1.2 堆优化版Dijkstra算法
堆优化版本更加符合这个视频里描绘的过程
// 稀疏图加边 void add(int x, int y, int z) { e[idx] = y; // e存当前节点的下标 w[idx] = z; // w存边的权重 ne[idx] = h[x]; // ne存下一个节点下标 h[x] = idx++; // h[x] 表示x为起点连线的终点下标 } int dijkstra(){ memset(dist,0x3f,sizeof dist); // 距离初始为无穷大 dist[1] = 0; // 起点距离为0,注意dist数组存的是 起点 到 当前点 的 最短距离 // 定义一个小根堆 priority_queue<PII,vector<PII>,greater<PII>> heap; heap.push({0,1}); // 用pair存,第一个是dist,第二个是点的下标,因为小根堆排序的时候先first,再second while(heap.size()) { PII t = heap.top(); // 每次取距离起点最小的点 heap.pop(); if(st[t.second]) continue; // 如果该点已被访问过,跳过 st[t.second] = true; for(int i = h[t.second]; i != -1; i = ne[i]) // 更新所有出边(朴素版是处理所有的边,但实际上只需要处理出边就好了) { int j = e[i]; // 注意h[t.second]和i存的是下标 if(dist[j] > t.first + w[i]){ dist[j] = t.first + w[i]; // 如果更新了节点,就要加入堆中,下次要从这些点里面找最小的距离点 heap.push({dist[j],j}); } } } if(dist[n] == 0x3f3f3f3f) return -1; else return dist[n]; } int main() { cin>>n>>m; memset(h,-1,sizeof h); //稀疏图初始化时,h全部置为-1 while(m--) { int x,y,z; cin>>x>>y>>z; add(x,y,z); } cout<<dijkstra(); return 0; }
2 Bellman-ford、SPFA
Bellman-ford算法适合处理带有负权边的图,SPFA是对这个算法的优化。
对于负权图的最短路径问题,如果存在负权回路则无解,所以一般题目不会有这种负权环的存在。可以借助这两个算法去判断一个回路有没有负权回路。
一般情况下,对于这种问题的最短路,Bellmon_ford要进行n-1次迭代,就能得到最终结果(n个点,我只需要确定了n-1条边就能找出最短路径)
bellman-ford算法适合解决有边数限制的最短路问题
2.1 Bellman-ford算法
int bellman_ford() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; for (int i = 0; i < k; i++) { //k次循环,表示最多经过k条边 memcpy(back, dist, sizeof dist); // back数组保存上次迭代结束后的结果,如果不保存,可能在迭代中,数组已经发生变化了,就会导致结果错误(串联效应) for (int j = 0; j < m; j++) { //遍历所有边 int a = e[j].a, b = e[j].b, w = e[j].w; dist[b] = min(dist[b], back[a] + w); // relax操作 } } if (dist[n] > 0x3f3f3f3f / 2) return -1; // 为什么要大于0x3f3f3f3f/2, 因为负权边的存在,可能出现INF+或-一个很小的数,但是实际上这种情况还是不可达的状态 else return dist[n]; }
2.2 SPFA算法
在relax操作中:dist[b] = min(dist[b], back[a] + w);
,我们发现,只有back[a]
变化的时候,dist[b]
才会发生变化,所以我们可以用一个队列来存,只有当一个点的dist发生变化的时候,我们把这个点压入队列中,然后更新他的所有出边即可。
第四点中,一般用SPFA的题目不会有负权回路,有负权回路就无解,虽然Bellman-ford算法能在负权回路的图中运行,但是结果是错的。你想想每次经过负权回路都会让你整条路径的权重减少,那么我算法就会无限走负权回路,你整条路径最终结果为-∞,显然不可能。
// int spfa() { memset(dist, 0x3f, sizeof dist); dist[1] = 0; queue<int> q; q.push(1); st[1] = true; // st表示已经入队了,避免重复入队 while (q.size()) { 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]) // relax操作 { dist[j] = dist[t] + w[i]; if (!st[j]) // 如果还没入队,则入队 { q.push(j); st[j] = true; } } } } return dist[n]; }
3 多源最短路 Floyd算法
多源最短路就是:每一个点到图中其他顶点的最短路
floyd算法中,d矩阵一开始存图,当算法结束后,这个d矩阵就会被更新为点与点之间的最短距离,所以你可以询问图中任意x,y
两点的最短距离,即d[x][y]
// 核心代码 void floyd() { for (int k = 1; k <= n; k ++ ) // 一定是k先循环 for (int i = 1; i <= n; i ++ ) //i,j循环谁先谁后无所谓 for (int j = 1; j <= n; j ++ ) d[i][j] = min(d[i][j], d[i][k] + d[k][j]); }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步