最短路
问题重现
问题来自 洛谷-P4779,选取问题时稍作了更改.
最短路算法
注:为了表示方便,我们将从 到点 之间的边的权重表示为 g[a][b],如果没有边则为 INF.
在介绍最短路之前,我们需要了解一个概念:松弛
我们已知有两个点 、,它们之间的最短距离为 g[a][b].我们尝试使用第三个点 ,如果从 点经过 点再到 点比从 点直接到 点所需要的路程少,则将最短距离更新.这样的操作叫做使用 对从点 到点 之间的边进行松弛.
使用代码表示如下:
g[a][b] = max(g[a][b], g[a][c] + g[c][b]);Floyd 算法
Floyd 算法很简单,它的本质是动态规划.
我们首先定义一个二维数组 dist[i][j],表示从点 到点 的最短距离.输入数据时,类似邻接矩阵,直接将数据输入 dist 数组.
而后,枚举每一个 , , ,尝试使用点 对从点 到点 之间的边进行松弛.
枚举完成后,此时的 dist 数组就是最终的结果.
Floyd 算法不仅可以解决单源最短路的题目,还可以解决多源单目标最短路的问题.
#include <bits/stdc++.h>#define N (int)1e5 // 最多可能有的节点数#define M (int)2e5 // 最多可能有的边数#define INF 0xffffff // 极大值,代表无穷using namespace std;int n, m, dist[N][N];void floyd(){ // 初始化数据 for (int i = 1; i <= n; i++){ for (int j = 1; j <= n; j++){ if (i == j) dist[i][j] = 0; // 自己到自己的距离为0 else dist[i][j] = INF; } }
for (int k = 1; k <= n; k++) // k 一定要在最外层 for(int i = 1; i <= n; i++) for(int j = 1; j <= n; j++) dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j]); // 尝试使用点k对从点i到点j之间的边进行松弛}int main(){ int s; cin >> n >> m >> s;
// 输入数据 for (int i = 1; i <= m; i++){ int u, v, w; cin >> u >> v >> w; dist[u][v] = w; }
floyd();
for (int i = 1; i <= n; i++) { if (dist[s][i] == INF) { cout << "2147483647 "; // 输出2^31-1 } else { cout << dist[s][i] << " "; } } cout << endl;}但是,Floyd 算法太慢了,时间复杂度到达了 ,因为基于邻接矩阵,空间复杂度也达到了 ,所以只能拿到部分分.
SPFA 算法
我们可以定义一个数组 dist[i],表示从源点 到 的最小距离.初始时每一项都是无穷大,而后将 dist[s] 设定为 0,表示从源点 到本身的最小距离为 0.之后,对于每个目标点 (一开始是 )的每一个相邻节点 ,都尝试使用点 对从点 到点 的边进行松弛.而后,将 选中为目标点,重复.直到没有待被选中的点位置,此时的 dist 数组就是最终的结果,类似于广度优先搜索.
SPFA 算法对于处理带有负权的图是最快的.
以下是基于链式前向星的源代码:
int dist[N]; // dist[i]表示从源点到i的最小距离bool vis[N]; // 检查是否在队列中void spfa(int s){// 在dist数组中记录从s出发到每个点的最短路 for(int i = 1;i <= n;i++){ // 初始化 dist[i] = INF; // 从s到每一个点都暂时无法到达 vis[i] = false; // 每一个节点都没有被访问过 } queue<int> q; // 类似于bfs中的队列,储存待被松弛的目标点 q.push(s); // 将s设定为目标点 vis[s] = true; // s现在在队列中 dist[s] = 0; // 从源点s到本身的最小距离为0 while(!q.empty()){ // 直到q为空时才停止 int x = q.front(); // 将x设为当前的目标点 q.pop(); // 从待办清单中移除 vis[x] = false; // 移除标记 for(int i = fi[x]; i != 0; i = ne[i]){ // 使用链式前向星寻找所有x的相邻节点 int v = to[i], w = co[i]; if (dist[v] > dist[x] + w){ dist[v] = dist[x] + w; // 松弛 if (!vis[v]) { // 只有当v不在队列中时才将其加入队列 q.push(v); // 将v入队 vis[v] = true; // 将v标记为在队列中 } } } }}显然,SPFA 算法的时间复杂度在最坏的情况下为 ,仍旧无法拿到满分.
dijkstra 算法
我们仍旧可以像 SPFA 一样,定义一个 dist 数组,不过,在松弛的时候,我们需要选取 dist 数组中最小的一个节点 ,对于 的每一个相邻节点 ,都尝试使用 来对从节点 到节点 的这条边进行松弛,直到每一个点都被选中作为点 过,此时的 dist 数组就是最终的结果.
dijkstra 其实就是 SPFA 的贪心版本.
以下是基于链式前向星的源代码:
int dist[N];bool vis[N]; // 检查是否在已经被选中过void dijkstra(int s){// 以s为原点进行松弛 for(int i = 1;i <= n;i++){ // 初始化 dist[i] = INF; // 从s到每一个点都暂时无法到达 vis[i] = false; // 每一个节点都没有被访问过 } dist[s] = 0; // s到自己的距离为0 for (int i = 1; i <= n; i++) // 依次选择每一个节点 { // 找出dist中最小且没有被选择过的一项 int minn = -1; // minn表示最小的一项的索引 for(int j = 1; j <= n; j++){ if (vis[j]) continue; // 如果被选择过,则不会再次被选择 if (minn == -1 || dist[j] < dist[minn]) minn = j; }
if (dist[minn] == INF) continue; // 如果被选择的这个节点无法到达,则这个节点不能被选中
// 使用链式前向星进行遍历 for(int j = fi[minn]; j != 0; j = ne[j]) { // 松弛 dist[to[j]] = min(dist[to[j]], dist[minn] + co[j]); } vis[minn] = true; // 将minn这个节点标记为已被选择过 }}显然,dijkstra 算法的时间复杂度在最坏的情况下为 ,仍旧无法拿到满分.此外,此算法无法处理含有负权边的图.
dijkstra 算法优化
dijkstra 算法可以利用堆进行优化.
宏观整个算法,最浪费时间的事情就是找出 dist 中最小的一项,因此我们就想到了使用堆来优化.
在赛场上直接实现堆不太现实,因此我们可以使用 C++STL 优先队列来快速查找最小的一项.
以下是基于链式前向星的源代码:
int dist[N];bool vis[N]; // 检查是否在队列中
struct Node { int dist; // 距离 int id; // 节点编号 friend bool operator<(Node a, Node b){ return a.dist > b.dist; // 此时如果返回true,则表示a的优先级低于b } // 由于优先队列是大的元素优先,所以需要重载运算符,使得dist小的元素优先};
void dijkstra(int s){ // 初始化 memset(dist, 0x3f, sizeof(dist)); memset(vis, 0, sizeof(vis));
priority_queue<Node> q; // 优先队列,用于存储节点 dist[s] = 0; // s点到s点的距离为0 q.push({0, s}); // 将s点加入队列 while(!q.empty()){ // 当队列为空时,立即结束 Node tmp = q.top(); // tmp为队列中最小的元素,获得最小的元素 q.pop(); // 将最小的元素弹出 int x = tmp.id; // x为获得的最小的元素的节点编号 if (vis[x]) continue; // 如果曾经被访问过,就跳过不访问 vis[x] = true; // 标记为访问过 for (int i = fi[x]; i != 0; i = ne[i]){ // 遍历所有以x为起点的边 int v = to[i], w = co[i]; // v为终点,w为权值 if (dist[v] > dist[x] + w){ // 松弛操作:如果从s到v的距离大于从s到x的距离加上x到v的距离 dist[v] = dist[x] + w; // 就更新从s到v的距离 q.push({dist[v], v}); // 将v点加入队列 } } }}这样的算法可以达到时间复杂度是 ,可以完成这道题目.
最短路算法比较
| 算法 | 时间复杂度 | 是否可以有负权边 | 使用场景 |
|---|---|---|---|
| Floyd | 是 | 多源最短路 | |
| SPFA | 最差 | 是 | 在有负权时 |
| dijkstra | 否 | 建议使用其优化版 | |
| dijkstra 优化 | 否 | 没有负权时 |
标程
以下的代码在洛谷中提交后,100% 的样例是 AC 的.
#include <bits/stdc++.h>#define N (int)1e5 + 100#define M (int)2e5 + 100#define INF 0xffffffusing namespace std;
int n, m;
int cnt;int fi[N];int ne[M];int to[M];int co[M];
void add(int u, int v, int w){ cnt++; ne[cnt] = fi[u]; fi[u] = cnt; to[cnt] = v; co[cnt] = w;}
int dist[N];bool vis[N];
struct Node { int dist; int id; friend bool operator<(Node a, Node b){ return a.dist > b.dist; }};
void dijkstra(int s){ memset(dist, 0x3f, sizeof(dist)); memset(vis, 0, sizeof(vis));
priority_queue<Node> q; dist[s] = 0; q.push({0, s}); while(!q.empty()){ Node tmp = q.top(); q.pop(); int x = tmp.id; if (vis[x]) continue; vis[x] = true; for (int i = fi[x]; i != 0; i = ne[i]){ int v = to[i], w = co[i]; if (dist[v] > dist[x] + w){ dist[v] = dist[x] + w; q.push({dist[v], v}); } } }}
int main(){ int s; cin >> n >> m >> s;
for (int i = 1; i <= m; i++){ int u, v, w; cin >> u >> v >> w; add(u, v, w); }
dijkstra(s);
for (int i = 1; i <= n; i++) { if (dist[i] == INF) { cout << 2147483647 << " "; } else { cout << dist[i] << " "; } } cout << endl; return 0;}文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!


