← 返回题目列表

Dijkstra 算法怎么求单源最短路径?为什么不能处理负权边?

高频 中等 第 14 / 30 题 更新于 2026/07/28
Dijkstra最短路径优先队列

简化版

Dijkstra 求「一个起点到所有点的最短路径」,用贪心 + 优先队列:每次从「还没确定的点里挑当前距离起点最近的」,把它的最短距离确定下来,再用它去松弛(更新)邻居的距离。因为它假设「已确定的点的最短距离不会再变小」,所以不能处理负权边(负权可能让已确定的点还能变得更短)。用小顶堆实现是 O((V+E) log V)。

详细版

int[] dijkstra(int n, List<int[]>[] adj, int src) { // adj[u] = {v, w}
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[src] = 0;
    // 小顶堆按距离排序:{距离, 节点}
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
    pq.offer(new int[]{0, src});
    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int d = cur[0], u = cur[1];
        if (d > dist[u]) continue;          // 过期的旧记录,跳过
        for (int[] e : adj[u]) {            // 松弛邻居
            int v = e[0], w = e[1];
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.offer(new int[]{dist[v], v});
            }
        }
    }
    return dist;
}

核心两步循环:取距离最小的未确定点 → 松弛它的邻居

完整版教学

一、Dijkstra 的贪心思想

Dijkstra 维护一个 dist[] 数组(起点到各点的当前最短距离)。它的贪心策略是:在所有”还没最终确定”的点里,当前 dist 最小的那个,它的 dist 就已经是最终最短距离了,可以确定下来。

为什么这个贪心成立?因为在非负权的图里,从起点到这个「当前最近点」的路径,不可能再通过其它更远的点绕出一条更短的路(绕路只会更长,因为边权非负)。确定它之后,用它去更新(松弛)邻居:如果「经过它到邻居」比邻居当前的 dist 更短,就更新。

二、松弛(relax)操作

松弛是最短路算法的通用动作:对边 (u, v, w),如果 dist[u] + w < dist[v],说明「先到 u 再到 v」比「当前记录的到 v」更短,就更新 dist[v] = dist[u] + w。Dijkstra 每确定一个点,就用它松弛所有邻居,逐步把最短距离「传播」出去。

三、为什么不能有负权边(核心)

Dijkstra 的正确性完全依赖「已确定的点,其最短距离不会再变小」 这个假设。而这个假设只在边权非负时成立。

如果存在负权边,一个已经被「确定」的点,可能后来通过一条负权边发现了更短的路径——但 Dijkstra 已经把它标记为确定、不再更新了,于是给出错误结果。

举个直觉例子:A→B 权 1,A→C 权 5,C→B 权 -10。Dijkstra 会先确定 B 的距离为 1(因为 1 < 5),但实际上 A→C→B = 5 + (-10) = -5 更短,已确定的 B 却不会再被修正。所以有负权边要用 Bellman-Ford 或 SPFA

四、优先队列优化

朴素 Dijkstra 每次「找当前最小的未确定点」要扫一遍所有点,O(V²)。用小顶堆(优先队列) 把「找最小」降到 O(log V):

  • 堆里存 {当前距离, 节点},每次弹出距离最小的。
  • 松弛成功就把新距离入堆。
  • 因为一个点可能被多次入堆(不同时刻的距离),弹出时要判断 d > dist[u] 跳过过期记录(懒删除)。

复杂度降到 O((V+E) log V),适合稀疏图。稠密图用朴素 O(V²) 反而可能更好。

五、复杂度与应用

  • 优先队列版:O((V+E) log V),稀疏图首选。
  • 朴素版:O(V²),稠密图可用。
  • 应用:地图导航(最短/最快路线)、网络路由(OSPF 协议基于 Dijkstra)、游戏寻路(配合 A*)。

六、常见误区与追问

考点正确口径
初始化源点距离 0,其余无穷大
贪心选择每次确定当前最短的未确定点
松弛用当前点尝试更新邻居距离
dist[src] = 0
while pq not empty:
  u = poll min dist
  for edge u -> v:
    if dist[v] > dist[u] + w:
      relax v

Dijkstra 的贪心成立依赖一个前提:边权非负,已确定点不会再被更短路径推翻。

  • 误区:Dijkstra 可以处理负权边。 负权边可能让已确定的最短距离之后变得更小,破坏贪心前提。
  • 误区:优先队列里一个点只能出现一次。 常见实现会多次入队,弹出旧距离时用 if d > dist[u] continue 跳过。
  • 误区:松弛就是访问邻居。 松弛是发现更短路径时更新 dist[v],并把新候选放入队列。
  • 追问:为什么不能用普通队列? 普通队列不能保证每次取出当前距离最小的点,带权图会错。
  • 追问:复杂度是多少? 邻接表加优先队列常写 O((V+E) log V),具体与堆实现有关。
  • 追问:有负权边该用什么? 单源可用 Bellman-Ford 或 SPFA 变体,所有点对可考虑 Floyd。

七、加强记忆

Dijkstra 求单源最短路(非负权)贪心 + 优先队列——每次取「未确定点中 dist 最小的」确定其最短距离,再松弛其邻居。不能处理负权边:它假设「已确定点的距离不再变小」,负权会打破这个假设导致错误(负权用 Bellman-Ford)。小顶堆优化到 O((V+E) log V),弹出时跳过过期记录。应用于导航、路由、寻路。