最短路问题
最短路问题
最短路算法(Shortest Path Algorithm)是用来解决图中两点之间的最短路径的问题。常见的应用包括:地图导航、网络路由、游戏寻路等。根据图的性质(有向/无向、是否有负权边)和需求(单源/多源),常见的最短路算法有以下几种:
🚩1. Dijkstra 算法(迪杰斯特拉)
适用:边权非负、单源最短路
-
思路 :贪心 + 堆优化
从起点出发,每次选择当前距离起点最近的未访问点,更新它的邻居。
-
复杂度:
- 普通实现:O(n²)
- 堆优化:O(m log n)(用优先队列)
cpp
priority_queue<pair<int, int>, vector<pair<int, int>>, greater<>> pq;
vector<int> dist(n+1, INF);
dist[start] = 0;
pq.push({0, start});
while (!pq.empty()) {
auto [d, u] = pq.top(); pq.pop();
if (d > dist[u]) continue;
for (auto [v, w] : adj[u]) {
if (dist[v] > dist[u] + w) {
dist[v] = dist[u] + w;
pq.push({dist[v], v});
}
}
}
🚩2. Bellman-Ford 算法
适用:可处理负权边,单源最短路
- 思路 :动态规划思想,最多做 n-1 轮松弛操作
- 可检测负权环
- 复杂度:O(nm)
cpp
vector<int> dist(n+1, INF);
dist[start] = 0;
for (int i = 1; i < n; ++i)
for (auto [u, v, w] : edges)
if (dist[u] + w < dist[v])
dist[v] = dist[u] + w;
🚩3. SPFA 算法(队列优化的 Bellman-Ford)
适用:单源最短路,能处理负权边,效率比 Bellman-Ford 好很多(在稀疏图中)
- 思路:维护一个队列,只处理"可能更新别人的点"
- 复杂度:最坏 O(nm),实际通常很快
🚩4. Floyd-Warshall 算法
适用:多源最短路径(任意两点)
- 思路:动态规划,三层循环枚举中转点 k、起点 i、终点 j
- 复杂度:O(n³)
- 能处理负边权,但不能有负权环
cpp
for (int k = 1; k <= n; ++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]);
🚩5. A* 算法(启发式最短路)
适用:用于地图类路径规划,效率高于 Dijkstra(例如游戏寻路)
- 思路:在 Dijkstra 的基础上,加一个"估价函数 h(x)"(例如曼哈顿距离),引导搜索方向
- 复杂度:依赖启发函数,理论上最坏仍是 O(n log n)
🧠如何选择算法?
图类型/需求 | 推荐算法 |
---|---|
单源、无负权 | Dijkstra |
单源、有负权 | Bellman-Ford / SPFA |
多源任意两点 | Floyd-Warshall |
实时导航/地图 | A* |
3.6Dijkstra
3.6.1Dijkstra求最短路I
给定一个 n个点 m条边的有向图,图中可能存在重边和自环,所有边权均为正值。
请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 −1。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
输出格式
输出一个整数,表示 11 号点到 nn 号点的最短距离。
如果路径不存在,则输出 −1。
数据范围
1≤n≤500,
1≤m≤1e5,
图中涉及边长均不超过10000。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
cpp
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N=1e6+10;
const int M=2*N;
int h[N],e[N],w[N],ne[N],idx;
int g[510][510];
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 dijkstra1()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1;i<=n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
if(!st[j]&&(t==-1||dist[j]<dist[t])) 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;
return dist[n];
}
//邻接表法
int dijkstra2()
{
memset(dist,0x3f,sizeof dist);
dist[1]=0;
for(int i=1;i<=n;i++)
{
int t=-1;
for(int j=1;j<=n;j++)
if(!st[j]&&(t==-1||dist[j]<dist[t])) t=j;
st[t]=true;
for(int j=h[t];j!=-1;j=ne[j])
{
int k=e[j];
dist[k]=min(dist[k],dist[t]+w[j]);
}
}
if(dist[n]==0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
memset(h,-1,sizeof h);
memset(g,0x3f,sizeof g);
cin>>n>>m;
for(int i=1;i<=m;i++)
{
int a,b,c;
cin>>a>>b>>c;
g[a][b]=min(g[a][b],c);
add(a,b,c);
}
cout<<dijkstra2();
return 0;
}
3.6.2Dijkstra求最短路II
给定一个 n个点 m条边的有向图,图中可能存在重边和自环,所有边权均为非负值。
请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 −1。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数x,y,z,表示存在一条从点 x 到点 y 的有向边,边长为 z。
输出格式
输出一个整数,表示 1号点到 n号点的最短距离。
如果路径不存在,则输出−1。
数据范围
1≤n,m≤1.5×1e5,
图中涉及边长均不小于 0,且不超过10000。
数据保证:如果最短路存在,则最短路的长度不超过 1e9。
输入样例:
3 3
1 2 2
2 3 1
1 3 4
输出样例:
3
用小根堆即可,我们把每次求离起点最近进行了堆优化,可以缩短时间。
cpp
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>//堆的头文件
using namespace std;
typedef pair<int, int> PII;//堆里存储距离和节点编号
const int N = 1e6 + 10;
int n, m;//节点数量和边数
int h[N], w[N], e[N], ne[N], idx;//邻接矩阵存储图
int dist[N];//存储距离
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()
{
memset(dist, 0x3f, sizeof dist);//距离初始化为无穷大
dist[1] = 0;
priority_queue<PII, vector<PII>, greater<PII>> heap;//小根堆
heap.push({0, 1});//插入距离和节点编号
while (heap.size())
{
auto t = heap.top();//取距离源点最近的点
heap.pop();
int ver = t.second, distance = t.first;//ver:节点编号,distance:源点距离ver 的距离
if (st[ver]) continue;//如果距离已经确定,则跳过该点
st[ver] = true;
for (int i = h[ver]; i != -1; i = ne[i])//更新ver所指向的节点距离
{
int j = e[i];
if (dist[j] > dist[ver] + w[i])
{
dist[j] = dist[ver] + w[i];
heap.push({dist[j], j});//距离变小,则入堆
}
}
}
if (dist[n] == 0x3f3f3f3f) return -1;
return dist[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
cout << dijkstra() << endl;
return 0;
}
3.7bellman-ford
3.7.1有边数限制的最短路
给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出从 1号点到 n号点的最多经过 k条边的最短距离,如果无法从 1号点走到 n号点,输出 impossible。
注意:图中可能 存在负权回路 。
输入格式
第一行包含三个整数 n,m,k。
接下来 m行,每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
点的编号为 1∼n。
输出格式
输出一个整数,表示从 1号点到 n号点的最多经过 k条边的最短距离。
如果不存在满足条件的路径,则输出 impossible。
数据范围
1≤n,k≤500,
1≤m≤10000,
1≤x,y≤n,
任意边长的绝对值不超过10000。
输入样例:
3 3 1
1 2 1
2 3 1
1 3 3
输出样例:
3
思路:
cpp
#include <cstring>
#include <iostream>
#include <algorithm>
using namespace std;
const int N = 510, M = 10010;
struct Edge
{
int a, b, c;
}edges[M];
int n, m, k;
int dist[N];
int last[N];
void bellman_ford()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
for (int i = 0; i < k; i ++ )
{
memcpy(last, dist, sizeof dist);
for (int j = 0; j < m; j ++ )
{
auto e = edges[j];
dist[e.b] = min(dist[e.b], last[e.a] + e.c);
}
}
}
int main()
{
scanf("%d%d%d", &n, &m, &k);
for (int i = 0; i < m; i ++ )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
edges[i] = {a, b, c};
}
bellman_ford();
if (dist[n] > 0x3f3f3f3f / 2) puts("impossible");//可能存在负权回路,可能在求最短路径的时候
//存在负权边,把正无穷相对于原来的正无穷减少,所以要>0x3f3f3f3f/2
else printf("%d\n", dist[n]);
return 0;
}
3.8spfa
3.8.1spfa求最短路
给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你求出 1号点到 n号点的最短距离,如果无法从 1号点走到 n号点,则输出 impossible。
数据保证不存在负权回路。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
输出格式
输出一个整数,表示 1号点到 n号点的最短距离。
如果路径不存在,则输出 impossible。
数据范围
1≤n,m≤1e5,
图中涉及边长绝对值均不超过10000。
输入样例:
3 3
1 2 5
2 3 -3
1 3 4
输出样例:
2
spfa用的最多
cpp
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 100010;
int n, m;
int h[N], w[N], e[N], ne[N], idx;
int dist[N];
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 spfa()
{
memset(dist, 0x3f, sizeof dist);
dist[1] = 0;
queue<int> q;
q.push(1);
st[1] = true;
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])
{
dist[j] = dist[t] + w[i];
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n];
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
int t = spfa();
if (t == 0x3f3f3f3f) puts("impossible");
else printf("%d\n", t);
return 0;
}
3.8.2spfa判断负环
给定一个 n个点 m条边的有向图,图中可能存在重边和自环, 边权可能为负数。
请你判断图中是否存在负权回路。
输入格式
第一行包含整数 n和 m。
接下来 m行每行包含三个整数 x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
输出格式
如果图中存在负权回路,则输出 Yes,否则输出 No。
数据范围
1≤n≤2000,
1≤m≤10000,
图中涉及边长绝对值均不超过 10000。
输入样例:
3 3
1 2 -1
2 3 4
3 1 -4
输出样例:
Yes
cpp
#include <cstring>
#include <iostream>
#include <algorithm>
#include <queue>
using namespace std;
const int N = 2010, M = 10010;
int n, m;
int h[N], w[M], e[M], ne[M], idx;
int dist[N], cnt[N];
bool st[N];
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx ++ ;
}
bool spfa()
{
queue<int> q;
for (int i = 1; i <= n; i ++ )
{
st[i] = true;
q.push(i);
}
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])
{
dist[j] = dist[t] + w[i];
cnt[j] = cnt[t] + 1;
if (cnt[j] >= n) return true;
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return false;
}
int main()
{
scanf("%d%d", &n, &m);
memset(h, -1, sizeof h);
while (m -- )
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c);
}
if (spfa()) puts("Yes");
else puts("No");
return 0;
}
3.9Floyd
3.9.1Floyd求最短路
给定一个 n个点 m条边的有向图,图中可能存在重边和自环,边权可能为负数。
再给定 k个询问,每个询问包含两个整数 x 和 y,表示查询从点 x到点 y的最短距离,如果路径不存在,则输出 impossible。
数据保证图中不存在负权回路。
输入格式
第一行包含三个整数 n,m,k。
接下来 m行,每行包含三个整数x,y,z,表示存在一条从点 x到点 y的有向边,边长为 z。
接下来 k 行,每行包含两个整数 x,y,表示询问点 x到点 y的最短距离。
输出格式
共 k行,每行输出一个整数,表示询问的结果,若询问两点间不存在路径,则输出 impossible。
数据范围
1≤n≤200,
1≤k≤n2
1≤m≤20000,
图中涉及边长绝对值均不超过10000。
输入样例:
3 3 2
1 2 1
2 3 2
1 3 1
2 1
1 3
输出样例:
impossible
1
多源汇最短路
cpp
#include<iostream>
#include<algorithm>
#include<cstring>
#include<cstdio>
using namespace std;
const int N = 210, INF = 1e9;
int n, m, Q;
int d[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]);
}
int main()
{
scanf("%d%d%d", &n, &m, &Q);
for (int i = 1; i <= n; i++)
for (int j = 1; j <= n; j++)
if (i == j) d[i][j] = 0;
else d[i][j] = INF;
while (m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
d[a][b] = min(d[a][b], c);
}
floyd();
while (Q--)
{
int a, b;
scanf("%d%d", &a, &b);
int t = d[a][b];
if (t > INF / 2) puts("impossible");
else printf("%d\n", t);
}
return 0;
}