一、最短路径问题:从地图导航到网络路由

最短路径,说白了就是「怎么走最近」。你打开高德地图查路线,后台跑的就是这类算法。我在做分布式系统时,经常要计算节点间的最小延迟路径——嗯,那时候我才真正体会到,选对算法比写对代码重要得多。

今天咱们聊三个最经典的:DijkstraBellman-FordFloyd-Warshall。它们各有脾气,适用场景完全不同。

核心区别一句话总结:

  • Dijkstra:单源 + 非负权 → 最快
  • Bellman-Ford:单源 + 可负权 → 能检测负环
  • Floyd-Warshall:全源 → 稠密图首选
最短路径算法全景图 Dijkstra 单源 · 非负权 · 贪心 Bellman-Ford 单源 · 可负权 · 动态规划 Floyd-Warshall 全源 · 可负权 · 动态规划 核心特性对比 时间复杂度 O((V+E)logV) O(V·E) O(V³) 负权处理 ✗ 不支持 ✓ 支持 ✓ 支持 负环检测 适用场景 地图导航 网络路由 多源查询 选算法 = 看图结构 + 看需求

二、Dijkstra:最常用的单源最短路径

Dijkstra 是我个人用得最多的算法。它像个贪心的导游——每次都选当前最近的点,然后更新邻居。前提是:所有边的权重不能为负。

2.1 算法思想

维护一个距离数组 dist[],初始时 dist[起点]=0,其他为无穷大。每次从「未访问」的节点中挑一个距离最小的,用它去松弛邻居。重复直到所有节点都访问过。

我的小技巧:用优先队列(最小堆)来选当前最小距离节点,能把时间复杂度从 O(V²) 降到 O((V+E)logV)。面试时一定要提这个优化。

2.2 代码模板

// Dijkstra 标准模板(邻接表 + 优先队列)
public int[] dijkstra(int n, List<int[]>[] graph, int start) {
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[start] = 0;

    // 最小堆:[距离, 节点]
    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
    pq.offer(new int[]{0, start});

    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int d = cur[0], u = cur[1];

        // 重要:跳过过时记录
        if (d > dist[u]) continue;

        for (int[] edge : graph[u]) {
            int v = edge[0], w = edge[1];
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.offer(new int[]{dist[v], v});
            }
        }
    }
    return dist;
}

我曾经踩过的坑:有一次忘了写 if (d > dist[u]) continue;,结果优先队列里塞满了过期的旧记录,性能直接崩了。这个剪枝一定要加。

三、Bellman-Ford:能处理负权边的硬汉

Bellman-Ford 的思路更暴力——我不管顺序,我就对所有边做 V-1 轮松弛。为什么是 V-1?因为最短路径最多包含 V-1 条边,再多就有环了。

3.1 算法思想

说白了就是「暴力松弛」。每一轮遍历所有边,尝试更新距离。做完 V-1 轮后,再跑一轮——如果还能更新,说明存在负权环。

3.2 代码模板

// Bellman-Ford 标准模板
public int[] bellmanFord(int n, int[][] edges, int start) {
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[start] = 0;

    // 做 V-1 轮松弛
    for (int i = 0; i < n - 1; i++) {
        boolean updated = false;
        for (int[] edge : edges) {
            int u = edge[0], v = edge[1], w = edge[2];
            if (dist[u] != Integer.MAX_VALUE && dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                updated = true;
            }
        }
        // 优化:如果某轮没更新,提前结束
        if (!updated) break;
    }

    // 检测负权环
    for (int[] edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        if (dist[u] != Integer.MAX_VALUE && dist[u] + w < dist[v]) {
            // 存在负权环
            return new int[]{-1};
        }
    }
    return dist;
}

注意:Bellman-Ford 的时间复杂度是 O(V·E),在稀疏图上还行。但如果是稠密图,E 接近 V²,那就 O(V³) 了——这时候不如用 Floyd-Warshall。

四、Floyd-Warshall:全源最短路径的终极方案

Floyd-Warshall 的思路很优雅——我逐个考虑每个节点作为「中间点」,看看能不能让路径更短。你想想看,这其实是个三维的动态规划,只不过压缩成了二维数组。

4.1 算法思想

定义 dp[k][i][j] 表示「只允许使用前 k 个节点作为中间点时,i 到 j 的最短距离」。状态转移:要么不走 k,要么走 k。压缩后就是:dist[i][j] = min(dist[i][j], dist[i][k] + dist[k][j])

4.2 代码模板

// Floyd-Warshall 标准模板
public int[][] floydWarshall(int n, int[][] edges) {
    // 初始化距离矩阵
    int[][] dist = new int[n][n];
    for (int i = 0; i < n; i++) {
        Arrays.fill(dist[i], Integer.MAX_VALUE / 2);
        dist[i][i] = 0;
    }
    for (int[] edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        dist[u][v] = Math.min(dist[u][v], w);  // 处理重边
    }

    // 核心三重循环
    for (int k = 0; k < n; k++) {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (dist[i][k] + dist[k][j] < dist[i][j]) {
                    dist[i][j] = dist[i][k] + dist[k][j];
                }
            }
        }
    }
    return dist;
}

我的经验:Floyd 的三重循环顺序不能乱——k 必须在最外层。我见过有人写成 i、j、k 的顺序,结果算出来全是错的。记住:k 是「中间点」,必须一层层扩展。

五、经典例题:网络延迟时间

这道题是 LeetCode 743,也是我当年面试字节时遇到的原题。题目说:有 N 个网络节点,给你一个信号从 K 出发,问所有节点都收到信号需要多久?

说白了就是求 K 到所有节点的最短路径中的最大值。用 Dijkstra 最合适——因为网络延迟不可能是负数。

5.1 解题思路

  1. 建图:邻接表,每个节点存 [邻居, 延迟]
  2. 跑 Dijkstra,得到 dist[] 数组
  3. 取 dist[] 中的最大值——如果还有无穷大,说明有节点不可达,返回 -1

5.2 代码实现

public int networkDelayTime(int[][] times, int n, int k) {
    // 1. 建图
    List<int[]>[] graph = new ArrayList[n];
    for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
    for (int[] t : times) {
        int u = t[0] - 1, v = t[1] - 1, w = t[2];
        graph[u].add(new int[]{v, w});
    }

    // 2. Dijkstra
    int[] dist = new int[n];
    Arrays.fill(dist, Integer.MAX_VALUE);
    dist[k - 1] = 0;

    PriorityQueue<int[]> pq = new PriorityQueue<>((a, b) -> a[0] - b[0]);
    pq.offer(new int[]{0, k - 1});

    while (!pq.isEmpty()) {
        int[] cur = pq.poll();
        int d = cur[0], u = cur[1];
        if (d > dist[u]) continue;

        for (int[] edge : graph[u]) {
            int v = edge[0], w = edge[1];
            if (dist[u] + w < dist[v]) {
                dist[v] = dist[u] + w;
                pq.offer(new int[]{dist[v], v});
            }
        }
    }

    // 3. 取最大值
    int maxDelay = 0;
    for (int d : dist) {
        if (d == Integer.MAX_VALUE) return -1;
        maxDelay = Math.max(maxDelay, d);
    }
    return maxDelay;
}

注意:题目给的节点编号是从 1 到 N,代码里要记得减 1 转成 0 索引。我刚开始刷题时经常在这上面翻车。

六、三种算法怎么选?

场景 推荐算法 原因
单源 + 非负权 Dijkstra 最快,O((V+E)logV)
单源 + 可能有负权 Bellman-Ford 能检测负环,但慢
全源 + 稠密图 Floyd-Warshall 代码简单,O(V³) 可接受
全源 + 稀疏图 跑 V 次 Dijkstra 比 Floyd 快

嗯,到这里三种最短路径算法就聊完了。我个人建议:先把 Dijkstra 练熟,它是最常用的。Bellman-Ford 和 Floyd 理解思想就好,面试时能写出来就行。

一句话总结:Dijkstra 是日常主力,Bellman-Ford 是负权救星,Floyd 是全源利器。根据图的特点选,别硬套。

公众号:蓝海资料掘金营,微信 deep3321