- gf24240 的博客
《梦溪笔谈·笔记》图:最短路之二
- @ 2026-7-15 10:27:27
前言
一。
3. 经典最短路算法
松弛
对于起点 到终点 ,边权为 。如果 ( 表示 到起点的距离),则将 更新为 。这个过程就是松弛。
3 - 2. Dijkstra 单源最短路径
Dijkstra 其实就是 BFS + 贪心。每次找到到起点距离最短的点,进行拓展。但是实际上和 BFS 没什么关系。而且还和 Floyd 有点像。
朴素 Dijkstra
例如这张图:
-
设 为 到起点的距离。初始化时 全为 不可达。将 设为 。如图:
然后,遍历与 相连的点 ,对它们进行松弛。
- 。
- 。
-
然后,遍历 数组,找出 最小即到起点距离最小的点。,最小的是 。但 已经被使用过,所以松弛 。
- 。
- 。
-
。同时 已经被使用过,所以松弛 。
- 。
-
最后松弛 点,但 没有出边。Dijkstra 结束,最短路 。最后,。
这样,每次松弛遍历 数组里 个点,松弛 个点。时间复杂度 。
发现 Dijkstra 的计算量明显比 Floyd 少很多。代码:
void dij(int s)
{
for (int i = 1; i <= n; ++i)
dis[i] = INF;
dis[s] = 0;
for (int i = 1; i <= n; ++i)
{
int u = 0, t = INF;
for (int j = 1; j <= n; ++j)
{
if (!vis[j] && dis[j] < t)
{
t = dis[j];
u = j
}
}
vis[u] = 1;
for (auto nd : g[u])
{
int v = g[u].v, w = g[u].w;
if (dis[u] + w < dis[v])
dis[v] = dis[u] + w;
}
}
}
堆优化 Dijkstra
在朴素 Dijkstra 中,“找到距离最小的点”这个步骤可以使用一些能自动排序的数据结构来解决。优先队列(即堆,priority_queue)就是常用的一个。关于优先队列,可以看 这个。
如图:
开始时, 全为不可达。将 入队并将 设为 。
将优先队列设为按照 从小到大排序。直接取出队列头上的那个节点,时间复杂度 。这里是 。
弹出 。遍历与 相连的所有点 ,对它们进行松弛。这与朴素 Dijkstra 是一样的。
,将 入队。同理将 入队。如果设置正确,优先队列会按 从小到大排序如下表:
priority_queue
| dis | node |
|---|---|
| 2 | |
| 3 |
注意:priority_queue 默认是大根堆,即从大到小排序,具体见 这个。
每次都要拿出最短的点,时间 。这进行 次,因此总时间 。
每条边都有可能进行入队操作,时间 。这进行 次,总时间 。
综合就是 。代码:
void dij(int s)
{
for (int i = 1; i <= n; ++i)
dis[i] = INF;
q.push({s, 0});
dis[s] = 0;
while (!q.empty())
{
int u = q.top().v;
q.pop();
if (vis[u])continue;
vis[u] = 1;
for (auto nd : g[u])
{
int v = nd.v, w = nd.w;
if (dis[u] + w < dis[v])
{
dis[v] = dis[u] + w;
q.push({v, dis[v]});
}
}
}
}
对比
正常情况下,如果 和 接近, 可以看作 ,相比朴素 效率更高。
但堆优化也并非完全是优化。例如在这道题:P1186 玛丽卡 。当边数 足够大时,“所谓堆优化其实是劣化”。此时用朴素 Dijkstra 是更好的选择。
3 - 3. Bellman-Ford 单源最短路径
与 Floyd 和 Dijkstra 不同,Bellman-Ford 通过边来松弛。
例如这个图:
边:
| 1 | 2 | |
| 3 | ||
| 2 | ||
| 4 | ||
| 3 |
遍历每一条边,通过 松弛 。即 。
初始时,如图( 表示从起点到 的最短路):
- 对于边 :。
- 对于边 :。
- 对于边 :。
- 对于边 :。
- 对于边 :。
这确实是最好的情况,每次遍历边都能成功松弛,遍历完之后正是最短路。
但是实际上题目不会给出这么好的图的方式。例如:
边:
| 3 | 4 | |
| 2 | 3 | |
| 1 | 2 | |
| 2 | 4 | |
| 1 | 3 |
- 对于边 :两端都是 ,无法松弛。
- 对于边 :两端都是 ,无法松弛。
- 对于边 :。
- 对于边 :。
- 对于边 :。
这是正常情况。虽然图看上去一样,但边存储的顺序不一样结果就会不同。
为了保证求出最短路,这个过程要执行 次(每次至少松弛 个点,要松弛 个点就是 次)。这样才能求出最短路。时间复杂度:。代码:
bool bell()
{
memset(dis, 0x3f, sizeof(dis));
dis[1] = 0;
for (int i = 1; i <= n; ++i)
{
bool flag = 0;
for (int j = 1; j <= m; ++j)
{
int v = g[j].v, u = g[j].u, w = g[j].w;
if (i == n && dis[u] + w < dis[v])return 1;
if (dis[u] + w < dis[v])
{
dis[v] = dis[u] + w;
flag = 1;
}
}
if (!flag)break;
}
return 0;
}
Bellman-Ford 的好处就是:代码简单(跟 Floyd 差不多),存图不需要用邻接矩阵或邻接链表等。只需要存储上面的表格那样的。
3 - 4. SPFA 单源最短路径
SPFA 其实算法思想上和 Bellman-Ford 是一致的。只是使用队列来优化。
在 Bellman-Ford 中,会有大量的边两端都是 导致无法松弛。SPFA 就用队列解决了这个问题:将上一轮松弛成功的点入队。松弛后就出队。
例如这个图:
初始化 全为不可达,所有节点标记都为 未入队。将 入队、将 设为 并将 标记为 入队。注意这个标记可以改为 未入队,即节点可以重复入队。
遍历与 相连的点 。同时弹出 ,将 标记为 未入队。,将 入队,标记为 入队。节点 同理。
等到队列为空,SPFA 结束。
理论上,时间复杂度是 ,实际上会快很多。但也会有一些数据能让 SPFA 退化为 。代码:
bool spfa()
{
memset(dis, 0x3f, sizeof(dis));
memset(vis, 0, sizeof(vis));
memset(len, 0, sizeof(len));
while (!q.empty())q.pop();
q.push(1); vis[1] = 1;
dis[1] = 0;
while (!q.empty())
{
int u = q.front();
q.pop(); vis[u] = 0;
for (auto nd : g[u])
{
int v = nd.first, w = nd.second;
if (dis[u] + w < dis[v])
{
dis[v] = dis[u] + w;
len[v] = len[u] + 1;
if (len[v] >= n)return 1; // 判断负环
if (!vis[v])
{
q.push(v);
vis[v] = 1;
}
}
}
}
return 0;
}
3 - 3 & 4. 判断负环
负环:一个环,权值之和为负数。如果图中有负环,那就不存在最短路径。因为可以无限经过这个负环,让路径一直变小。
在没有负环的图中,最短路径最多经过所有点,即经过边数最长为 。如果最短路径边数能达到 ,那一定存在负环。
- Bellman-Ford 判断负环:正常的 Bellman-Ford 外层只需要循环 次(经过 条边)。如果循环到第 次还可以松弛,那一定存在负环。
- SPFA 判断负环:定义一个 表示这条最短路径上的边数。。如果 ,那一定存在负环。