题解:AcWing 851 SPFA求最短路
【题目来源】
AcWing:851 spfa求最短路 - AcWing题库
【题目描述】
给定一个\(n\)个点\(m\)条边的有向图,图中可能存在重边和自环,边权可能为负数。请你求出\(1\)号点到\(n\)号点的最短距离,如果无法从\(1\)号点走到\(n\)号点,则输出 impossible。数据保证不存在负权回路。
【输入】
第一行包含整数\(n\)和\(m\)。接下来\(m\)行每行包含三个整数 \(x,y,z\),表示存在一条从点\(x\)到点\(y\)的有向边,边长为\(z\)。
【输出】
输出一个整数,表示\(1\)号点到\(n\)号点的最短距离。如果路径不存在,则输出 impossible。
【输入样例】
3 3
1 2 5
2 3 -3
1 3 4
【输出样例】
2
【核心思想】
-
问题分析:给定有向图 \(G=(V,E)\),其中 \(|V|=n\),\(|E|=m\),边权可能为负数,但保证不存在负权回路。需要求从起点 \(1\) 到终点 \(n\) 的最短距离。这是一个带负权边的单源最短路径问题,由于边权可能为负,不能使用 Dijkstra 算法,而 SPFA 是 Bellman-Ford 算法的队列优化版本,效率更高。
-
算法选择:
- SPFA 算法(Shortest Path Faster Algorithm):Bellman-Ford 算法的队列优化版本,只松弛那些距离被更新过的节点的邻接边
- 距离数组 \(dist[]\):\(dist[i]\) 表示从起点到节点 \(i\) 的当前最短距离估计值
- 队列 \(q\):存储待松弛的节点,只有距离被更新的节点才会入队
- 标记数组 \(st[]\):标记节点是否在队列中,避免重复入队
- 邻接表存储:使用链式前向星存储图,空间复杂度 \(O(n + m)\)
-
关键步骤:
- 初始化:
- 读取 \(n\)(节点数)、\(m\)(边数)
- 初始化邻接表头数组 \(h[i] = -1\),边索引 \(idx = 0\)
- 读入 \(m\) 条边 \((x, y, z)\),使用
add(x, y, z)添加到邻接表 - 初始化距离数组 \(dist[i] = INF\),\(dist[1] = 0\)(起点距离为0)
- 初始化标记数组 \(st[i] = false\)
- 起点入队:
q.push(1),st[1] = true - 主循环(当队列非空时循环):
- 取出队首:
t = q.front(),然后q.pop(),st[t] = false - 松弛邻接边:遍历 \(t\) 的所有邻接边 \((t \to j,权重w)\):
- 若 \(dist[j] > dist[t] + w\),则更新 \(dist[j] = dist[t] + w\)
- 若 \(j\) 不在队列中(\(!st[j]\)),则将 \(j\) 入队
q.push(j),st[j] = true
- 取出队首:
- 返回结果:
- 若 \(dist[n] == INF\),说明不可达,输出
impossible - 否则输出 \(dist[n]\) 作为答案
- 若 \(dist[n] == INF\),说明不可达,输出
- 初始化:
-
时间/空间复杂度:
- 时间复杂度:平均 \(O(m)\),最坏 \(O(n \cdot m)\)(存在负权边时)
- 空间复杂度:\(O(n + m)\),邻接表 \(O(n + m)\),距离数组、标记数组、队列各 \(O(n)\)
- 适用于带负权边的单源最短路径问题,在稀疏图上效率很高
-
SPFA 算法的核心思想:
- 松弛操作:只有当某个节点的距离被更新时,它的邻接边才可能需要松弛,因此只将这些节点加入队列
- 队列优化:相比 Bellman-Ford 的盲目遍历所有边,SPFA 只处理可能产生更新的边,大幅减少无效操作
- 惰性更新:节点出队后才进行松弛,避免重复处理
- 负权边处理:可以正确处理负权边,但无法处理负权回路(需要额外判断)
- 适用场景:稀疏图、带负权边的单源最短路径问题
- 在随机数据上效率很高,但可以被特殊数据卡到最坏情况
【解题思路】

【解题思路2】

【算法标签】
SPFA
【代码详解】
#include <bits/stdc++.h>
using namespace std;
// 定义常量
const int N = 100010, M = N, INF = 1e9; // N为节点数上限,M为边数上限,INF为无穷大
// 定义邻接表相关数组
int h[N], e[M], w[M], ne[M], idx; // h存储每个节点的第一条边的索引,e存储边的终点,w存储边的权重,ne存储下一条边的索引,idx为边的计数器
int dist[N]; // dist存储从起点到每个节点的最短距离
queue<int> q; // 队列用于广度优先搜索
bool st[N]; // st标记节点是否在队列中
// 定义节点数和边数
int n, m;
// 添加边的函数
void add(int a, int b, int c) {
e[idx] = b, w[idx] = c; // 设置边的终点和权重
ne[idx] = h[a], h[a] = idx++; // 更新邻接表
}
// SPFA算法实现
int spfa() {
fill(dist, dist + N, INF); // 初始化距离为无穷大
dist[1] = 0; // 起点距离设为0
q.push(1); // 将起点加入队列
st[1] = true; // 标记起点在队列中
while (!q.empty()) {
int nd = q.front(); q.pop(); // 取出队首元素
st[nd] = false; // 标记该节点不在队列中
for (int i = h[nd]; i != -1; i = ne[i]) { // 遍历所有邻接边
int j = e[i]; // 邻接节点
if (dist[j] > dist[nd] + w[i]) { // 如果通过当前节点到达邻接节点更近
dist[j] = dist[nd] + w[i]; // 更新最短距离
if (!st[j]) { // 如果邻接节点不在队列中
q.push(j); // 将邻接节点加入队列
st[j] = true; // 标记邻接节点在队列中
}
}
}
}
return dist[n]; // 返回从起点到终点的最短距离
}
// 主函数
int main() {
cin >> n >> m; // 输入节点数和边数
memset(h, -1, sizeof(h)); // 初始化邻接表头指针为-1
while (m--) { // 读取所有边的信息
int a, b, c;
cin >> a >> b >> c;
add(a, b, c); // 添加边
}
int ans = spfa(); // 计算最短路径
if (ans == INF) cout << "impossible"; // 如果无法到达终点,输出"impossible"
else cout << dist[n]; // 否则输出最短距离
return 0;
}
#include <bits/stdc++.h>
using namespace std;
typedef pair<int, int> PII; // 存储距离和节点编号的对
const int N = 100010;
int n, m; // n: 节点数,m: 边数
int h[N], w[N], e[N], ne[N], idx; // 邻接表存储图
int dist[N]; // 存储从起点到每个点的最短距离
bool st[N]; // 标记节点是否在队列中
// 添加一条有向边 a->b,权重为c
void add(int a, int b, int c)
{
e[idx] = b, w[idx] = c, ne[idx] = h[a], h[a] = idx++;
}
// SPFA算法求从节点1到节点n的最短路径
int spfa()
{
memset(dist, 0x3f, sizeof(dist)); // 初始化距离为无穷大
dist[1] = 0; // 起点到自己的距离为0
queue<int> q; // 队列存储需要松弛的节点
q.push(1); // 起点入队
st[1] = true; // 标记起点在队列中
while (q.size()) // 队列不为空时循环
{
int t = q.front(); // 取出队首节点
q.pop();
st[t] = false; // 标记节点已出队
// 遍历节点t的所有邻接节点
for (int i = h[t]; i != -1; i = ne[i])
{
int j = e[i]; // 邻接节点编号
// 如果通过t到j的距离更短,更新距离
if (dist[j] > dist[t] + w[i])
{
dist[j] = dist[t] + w[i];
// 如果j不在队列中,将其加入队列
if (!st[j])
{
q.push(j);
st[j] = true;
}
}
}
}
return dist[n]; // 返回从起点到节点n的最短距离
}
int main()
{
scanf("%d%d", &n, &m); // 读入节点数和边数
memset(h, -1, sizeof(h)); // 初始化邻接表头指针为-1
while (m--)
{
int a, b, c;
scanf("%d%d%d", &a, &b, &c);
add(a, b, c); // 添加有向边
}
int t = spfa(); // 执行SPFA算法
if (t == 0x3f3f3f3f) // 如果距离为无穷大,说明不可达
{
puts("impossible");
}
else
{
printf("%d\n", t); // 输出最短距离
}
return 0;
}
【运行结果】
3 3
1 2 5
2 3 -3
1 3 4
2
浙公网安备 33010602011771号