来源:算法讲解065【必备】A星、Floyd、Bellman-Ford与SPFA · 2026-10-31
依据:视频字幕 2000 字
A\* 是带「启发式估价」的 Dijkstra,用于单源单汇最短路;Floyd 是 O(n³) 全源最短路(可带负边);Bellman-Ford 是「松弛 n−1 轮」的单源最短路(可带负边、可判负环);SPFA 是 Bellman-Ford 的队列优化版(平均快、最坏退化)。
dist[s][u] 排序,A\* 按 dist[s][u] + h(u) 排序,h 是「u 到 t 的估计距离」,从而优先朝 t 的方向扩展,减少无用扩展。g[u][v],适合稠密/Floyd)、邻接表(vector<vector<pair<int,int>>>,竞赛首选)、链式前向星(数组模拟,常数小但写起来啰嗦)。dist[v] > dist[u] + w(u,v),就更新 dist[v] = dist[u] + w(u,v)。所有最短路算法的核心都是反复松弛。h(u) ≤ 真实距离,且一般要求 一致(consistent):h(u) ≤ w(u,v) + h(v),保证每个点只出堆一次。greater<> 或存负值变「小根堆」。**A\* 为什么对**:把每个点的键值设为 f(u) = g(u) + h(u),其中 g(u) 是当前已知 s→u 的距离。若 h 可采纳,则 f(u) 是「经过 u 到 t 的路径长度」的下界。小根堆每次弹出 f 最小的点,当弹出的点是 t 时,它的 g(t) 就是最优——因为任何其他路径的 f 值都不小于当前 f(t) = g(t)(h(t)=0),而 f 又是真实路径长度的下界,所以不可能有更短的。若 h 还满足一致性,则每个点第一次出堆时 g 已最优,等价于「Dijkstra 换了个排序键」。
Floyd 为什么对:f[k][i][j] 表示「只允许经过编号 ≤ k 的中间点」时 i→j 的最短路。转移:f[k][i][j] = min(f[k-1][i][j], f[k-1][i][k] + f[k-1][k][j])——要么不经过 k,要么经过 k。滚动掉第一维就是三重循环 for k: for i: for j: d[i][j] = min(d[i][j], d[i][k]+d[k][j])。k 必须是最外层,因为第 k 层依赖第 k−1 层的完整结果。
Bellman-Ford 为什么对:最短路最多 n−1 条边(无负环时)。第 i 轮松弛保证「边数 ≤ i 的最短路」全部正确。做 n−1 轮即得答案;第 n 轮若还能松弛,说明存在负环。
SPFA 为什么对:Bellman-Ford 每轮把所有边都扫一遍,但只有「上一轮 dist 变小的点」出发的边才可能再松弛。用队列维护这些点即可。正确性同 Bellman-Ford,只是省掉了无效扫描。
**A\*(网格 4 邻接,曼哈顿距离)**
dist[s]=0,小根堆存 (f, g, x, y),f = g + h。g + w < dist[nx][ny],更新 dist,入堆 (dist + h, dist, nx, ny)。Floyd
d[i][j] = w(i,j),无边为 INF,d[i][i]=0。for k in 0..n-1: for i: for j: d[i][j] = min(d[i][j], d[i][k]+d[k][j])。d[i][i] < 0。Bellman-Ford
dist[s]=0,其余 INF。dist[u]+w < dist[v] 则更新。SPFA
dist[s]=0,inqueue[s]=true,队列 push s。inqueue[u]=false;对每条出边 (u,v,w):若 dist[u]+w < dist[v],更新;若 v 不在队列,push 并标记。记录 cnt[v](入队次数),若 cnt[v] ≥ n ⇒ 负环。网格(. 可走,# 墙),s=(0,0),t=(2,2),4 邻接,每步代价 1,h = 曼哈顿距离:
. . .
# . .
. . .
A\* 过程(堆中元素 (f,g,pos)):
| 步 | 弹出 | g | 新入堆 |
|---|---|---|---|
| 1 | (4,0,(0,0)) | 0 | (5,1,(0,1)), (5,1,(1,0)) |
| 2 | (5,1,(0,1)) | 1 | (5,2,(0,2)), (6,2,(1,1)) |
| 3 | (5,2,(0,2)) | 2 | (6,3,(1,2)) |
| 4 | (6,2,(1,1)) | 2 | (6,3,(2,1)) |
| 5 | (6,3,(1,2)) | 3 | (6,4,(2,2)) |
| 6 | (6,4,(2,2)) | 4 | 弹出 t,答案 4 |
对比 Dijkstra 会先扩展 (1,0) 等「远离目标」的点,A\* 因 f 值更小优先走靠近 t 的方向。
**Python(A\* 网格)**
import heapq
def astar(grid, s, t):
n, m = len(grid), len(grid[0])
def h(x, y): return abs(x - t[0]) + abs(y - t[1])
INF = float('inf')
dist = [[INF]*m for _ in range(n)]
dist[s[0]][s[1]] = 0
pq = [(h(*s), 0, s[0], s[1])]
vis = [[False]*m for _ in range(n)]
while pq:
f, g, x, y = heapq.heappop(pq)
if vis[x][y]: continue
vis[x][y] = True
if (x, y) == t: return g
for dx, dy in ((1,0),(-1,0),(0,1),(0,-1)):
nx, ny = x+dx, y+dy
if 0 <= nx < n and 0 <= ny < m and grid[nx][ny] != '#' and not vis[nx][ny]:
ng = g + 1
if ng < dist[nx][ny]:
dist[nx][ny] = ng
heapq.heappush(pq, (ng + h(nx, ny), ng, nx, ny))
return -1
Python(Floyd)
def floyd(d): # d[i][j] 已初始化,无边 INF,d[i][i]=0
n = len(d)
for k in range(n):
for i in range(n):
dik = d[i][k]
if dik == float('inf'): continue
for j in range(n):
if dik + d[k][j] < d[i][j]:
d[i][j] = dik + d[k][j]
return d
Python(SPFA)
from collections import deque
def spfa(n, adj, s):
INF = float('inf')
dist = [INF]*n; dist[s] = 0
inq = [False]*n; cnt = [0]*n
q = deque([s]); inq[s] = True
while q:
u = q.popleft(); inq[u] = False
for v, w in adj[u]:
if dist[u] + w < dist[v]:
dist[v] = dist[u] + w
cnt[v] += 1
if cnt[v] >= n: return None # 负环
if not inq[v]:
q.append(v); inq[v] = True
return dist
**C++(A\* 网格,竞赛风)**
#include <bits/stdc++.h>
using namespace std;
const int INF = 0x3f3f3f3f;
int n, m;
vector<string> g;
int sx, sy, tx, ty;
int h(int x, int y) { return abs(x - tx) + abs(y - ty); }
int astar() {
vector<vector<int>> dist(n, vector<int>(m, INF));
vector<vector<char>> vis(n, vector<char>(m, 0));
// (f, g, x, y),小根堆
priority_queue<tuple<int,int,int,int>, vector<tuple<int,int,int,int>>, greater<>> pq;
dist[sx][sy] = 0;
pq.emplace(h(sx, sy), 0, sx, sy);
int dx[4] = {1,-1,0,0}, dy[4] = {0,0,1,-1};
while (!pq.empty()) {
auto [f, d, x, y] = pq.top(); pq.pop();
if (vis[x][y]) continue;
vis[x][y] = 1;
if (x == tx && y == ty) return d;
for (int k = 0; k < 4; ++k) {
int nx = x + dx[k], ny = y + dy[k];
if (nx < 0 || nx >= n || ny < 0 || ny >= m) continue;
if (g[nx][ny] == '#' || vis[nx][ny]) continue;
int nd = d + 1;
if (nd < dist[nx][ny]) {
dist[nx][ny] = nd;
pq.emplace(nd + h(nx, ny), nd, nx, ny);
}
}
}
return -1;
}
C++(Floyd,滚动一维,注意 long long)
const long long INF = 0x3f3f3f3f3f3f3f3fLL;
void floyd(vector<vector<long long>>& d) {
int n = d.size();
for (int k = 0; k < n; ++k)
for (int i = 0; i < n; ++i) {
if (d[i][k] == INF) continue; // 剪枝
long long dik = d[i][k];
for (int j = 0; j < n; ++j)
if (dik + d[k][j] < d[i][j]) d[i][j] = dik + d[k][j];
}
}
C++(Bellman-Ford,边表)
struct Edge { int u, v; long long w; };
// 返回 false 表示存在从 s 可达的负环
bool bellman(int n, const vector<Edge>& es, int s, vector<long long>& dist) {
const long long INF = 0x3f3f3f3f3f3f3f3fLL;
dist.assign(n, INF); dist[s] = 0;
for (int it = 0; it < n - 1; ++it) {
bool upd = false;
for (auto& e : es)
if (dist[e.u] != INF && dist[e.u] + e.w < dist[e.v]) {
dist[e.v] = dist[e.u] + e.w; upd = true;
}
if (!upd) break; // 提前退出
}
for (auto& e : es)
if (dist[e.u] != INF && dist[e.u] + e.w < dist[e.v]) return false;
return true;
}
C++(SPFA,vector 邻接表)
// 返回 false 表示存在从 s 可达的负环
bool spfa(int n, const vector<vector<pair<int,long long>>>& adj, int s, vector<long long>& dist) {
const long long INF = 0x3f3f3f3f3f3f3f3fLL;
dist.assign(n, INF); dist[s] = 0;
vector<char> inq(n, 0);
vector<int> cnt(n, 0);
queue<int> q; q.push(s); inq[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop(); inq[u] = 0;
for (auto [v, w] : adj[u]) {
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (++cnt[v] >= n) return false; // 入队 ≥ n 次 ⇒ 负环
if (!inq[v]) { q.push(v); inq[v] = 1; }
}
}
}
return true;
}
| 算法 | 时间 | 空间 | 常数 |
|---|---|---|---|
| A\* | 取决于 h;h 越准越快,最坏同 Dijkstra O((V+E)logV) | O(V+E) | 堆操作,常数中等 |
| Floyd | O(n³),最好=最坏 | O(n²) | 三重循环,常数小,n≤500 可过 |
| Bellman-Ford | O(VE),最好 O(E)(一轮无更新) | O(V+E) | 小 |
| SPFA | 平均 O(kE),最坏 O(VE)(网格图/菊花图易被卡) | O(V+E) | 小,但可被构造数据卡到 TLE |
|/&);或需要求最小环、判负环(d[i][i]<0)。// ===== Floyd:n ≤ 500,long long 防溢出 =====
const long long INF = 0x3f3f3f3f3f3f3f3fLL;
void floyd(vector<vector<long long>>& d) {
int n = d.size();
for (int k = 0; k < n; ++k)
for (int i = 0; i < n; ++i) {
if (d[i][k] == INF) continue;
for (int j = 0; j < n; ++j)
if (d[i][k] + d[k][j] < d[i][j])
d[i][j] = d[i][k] + d[k][j];
}
}
// ===== SPFA:返回 false 表示有从 s 可达的负环 =====
bool spfa(int n, const vector<vector<pair<int,long long>>>& adj, int s, vector<long long>& dist) {
dist.assign(n, INF); dist[s] = 0;
vector<char> inq(n, 0);
vector<int> cnt(n, 0);
queue<int> q; q.push(s); inq[s] = 1;
while (!q.empty()) {
int u = q.front(); q.pop(); inq[u] = 0;
for (auto [v, w] : adj[u])
if (dist[u] + w < dist[v]) {
dist[v] = dist[u] + w;
if (++cnt[v] >= n) return false;
if (!inq[v]) q.push(v), inq[v] = 1;
}
}
return true;
}
for i: for j: for k 是错的(会得到「至多一条中间边」的假结果)。INF + INF 溢出成负数,必须 if (d[i][k]==INF) continue; 或 INF 取 0x3f3f3f3f 且用 long long。d[i][i] 初始化 0;若题给自环边权为负,要取 min。g 比较)。dist、inq、cnt、邻接表、vis 全部要清;Floyd 的 d 矩阵要重新初始化。int 溢出;Floyd 的 d 用 long long。d[i][j] |= d[i][k] & d[k][j],用 bitset 优化到 O(n³/64)。i<j 用 d[i][j] + w[i][k] + w[k][j] 更新答案。x_v - x_u ≤ w 建成边 u→v 权 w,跑最短路判负环;求最大解跑最长路。w' = w + h[u] - h[v] 跑 n 次 Dijkstra,处理负边稀疏图。#define int long long 导致 priority_queue 比较器/abs 出问题——别用。priority_queue 默认大根堆,A\*/Dijkstra 忘了 greater<> 或存负值。vis 在入堆时标记(错),应在弹出时标记。int 且 INF=1e9,1e9+1e9 溢出。inq 标记,同一点反复入队导致爆炸。vector 没 clear(),上一组数据污染下一组。# 和 . 搞反,或边界 nx<0||nx>=n 写漏一个。-1 但没区分「不可达」和「负环」。题 1:n=4 的有向图,边 0→1 (1), 1→2 (-2), 2→3 (1), 3→1 (1),从 0 出发,判断是否有负环,若有输出 -1。
环 1→2→3→1 权和 -2+1+1=0,非负环。但注意 0→1→2→3→1 绕一圈回到 1 距离不变,无负环。dist = [0, 1, -1, 0]。SPFA 中 cnt[1] 会到 2 但 < 4,正常结束。结论:无负环。
</details>
题 2:3×3 网格,s=(0,0),t=(2,2),中间 (1,1) 是墙,4 邻接,每步 1。用 A\*(曼哈顿 h)求最短路长度,并说明为什么 A\*