每日算法 — 使用java实现Floyd算法:全源最短路径与动态规划思想

Floyd-Warshall算法(简称Floyd算法)是解决全源最短路径(All-Pairs Shortest Path)问题的经典动态规划算法。与Dijkstra算法需要运行V次才能求出所有顶点对的最短路径不同,Floyd算法只需一次三重循环即可解决。本文将深入剖析其动态规划思想,并用Java实现带路径记录的完整版本。

问题定义

给定一个带权有向图G=(V, E),其中V是顶点集合,E是边集合,权重可以为正、负(但不能有负权环)。要求找出任意两个顶点之间的最短路径长度。

动态规划思想

Floyd算法的核心状态定义为:

dp[k][i][j] = 从顶点i到顶点j,只经过编号不超过k的顶点作为中间点时的最短路径长度

状态转移方程:

dp[k][i][j] = min(dp[k-1][i][j], dp[k-1][i][k] + dp[k-1][k][j])

即:从i到j的最短路径,要么不经过顶点k,要么经过顶点k(由i→k和k→j两段组成)。

由于dp[k]只依赖于dp[k-1],可以用二维数组滚动优化,去掉第一维。

Java实现

import java.util.Arrays;

/**
 * Floyd-Warshall算法实现
 * 求解全源最短路径,支持负权边(不支持负权环)
 */
public class FloydWarshall {

    private static final int INF = Integer.MAX_VALUE / 2; // 避免溢出的大数

    int vertices;
    int[][] dist;      // 距离矩阵
    int[][] next;      // 路径记录矩阵,用于重构最短路径

    public FloydWarshall(int vertices) {
        this.vertices = vertices;
        dist = new int[vertices][vertices];
        next = new int[vertices][vertices];

        // 初始化距离矩阵
        for (int i = 0; i < vertices; i++) {
            Arrays.fill(dist[i], INF);
            dist[i][i] = 0; // 自身到自身距离为0

            Arrays.fill(next[i], -1);
        }
    }

    /**
     * 添加有向边
     * @param from 起点
     * @param to 终点
     * @param weight 权重
     */
    public void addEdge(int from, int to, int weight) {
        dist[from][to] = weight;
        next[from][to] = to; // 记录下一步走向
    }

    /**
     * Floyd-Warshall核心算法
     * 时间复杂度: O(V³)
     * 空间复杂度: O(V²)
     */
    public void computeShortestPaths() {
        // 三重循环:k为中间顶点,i为起点,j为终点
        for (int k = 0; k < vertices; k++) {
            for (int i = 0; i < vertices; i++) {
                for (int j = 0; j < vertices; j++) {
                    // 如果经过k能缩短i到j的距离,则更新
                    if (dist[i][k] + dist[k][j] < dist[i][j]) {
                        dist[i][j] = dist[i][k] + dist[k][j];
                        next[i][j] = next[i][k]; // 更新路径:i→...→k→...→j
                    }
                }
            }
        }
    }

    /**
     * 检测图中是否存在负权环
     * @return true表示存在负权环
     */
    public boolean hasNegativeCycle() {
        for (int i = 0; i < vertices; i++) {
            if (dist[i][i] < 0) {
                return true; // 顶点到自身的距离为负,说明存在负权环
            }
        }
        return false;
    }

    /**
     * 重构从start到end的最短路径
     * @param start 起点
     * @param end 终点
     * @return 路径上的顶点列表,如果不可达返回空数组
     */
    public int[] reconstructPath(int start, int end) {
        if (dist[start][end] == INF) {
            return new int[0]; // 不可达
        }

        // 先统计路径长度
        int pathLength = 1;
        int current = start;
        while (current != end) {
            current = next[current][end];
            pathLength++;
        }

        // 填充路径
        int[] path = new int[pathLength];
        path[0] = start;
        current = start;
        int index = 1;
        while (current != end) {
            current = next[current][end];
            path[index++] = current;
        }

        return path;
    }

    /**
     * 打印距离矩阵
     */
    public void printDistanceMatrix() {
        System.out.println("=== 最短路径距离矩阵 ===");
        for (int i = 0; i < vertices; i++) {
            for (int j = 0; j < vertices; j++) {
                if (dist[i][j] == INF) {
                    System.out.print("  INF ");
                } else {
                    System.out.printf(" %4d ", dist[i][j]);
                }
            }
            System.out.println();
        }
    }

    /**
     * 传递闭包:判断任意两点是否可达
     * 将距离矩阵改为布尔矩阵即可
     */
    public boolean[][] transitiveClosure() {
        boolean[][] reachable = new boolean[vertices][vertices];

        // 初始化
        for (int i = 0; i < vertices; i++) {
            for (int j = 0; j < vertices; j++) {
                reachable[i][j] = (i == j) || (dist[i][j] != INF);
            }
        }

        // Floyd-Warshall变体
        for (int k = 0; k < vertices; k++) {
            for (int i = 0; i < vertices; i++) {
                for (int j = 0; j < vertices; j++) {
                    reachable[i][j] = reachable[i][j] || (reachable[i][k] && reachable[k][j]);
                }
            }
        }

        return reachable;
    }

    public static void main(String[] args) {
        // 构建示例图(4个顶点)
        FloydWarshall fw = new FloydWarshall(4);
        fw.addEdge(0, 1, 5);
        fw.addEdge(0, 3, 10);
        fw.addEdge(1, 2, 3);
        fw.addEdge(2, 3, 1);
        fw.addEdge(3, 1, 2); // 形成环
        fw.addEdge(1, 3, 9);

        System.out.println("运行Floyd-Warshall算法...\n");
        fw.computeShortestPaths();

        if (fw.hasNegativeCycle()) {
            System.out.println("图中存在负权环!");
            return;
        }

        fw.printDistanceMatrix();

        System.out.println("\n=== 具体路径示例 ===");
        int[][] testPairs = {{0, 3}, {0, 2}, {3, 2}};
        for (int[] pair : testPairs) {
            int start = pair[0], end = pair[1];
            int[] path = fw.reconstructPath(start, end);
            System.out.printf("顶点 %d → 顶点 %d: 距离=%d, 路径=", 
                start, end, fw.dist[start][end]);
            for (int i = 0; i < path.length; i++) {
                System.out.print(path[i]);
                if (i < path.length - 1) System.out.print(" → ");
            }
            System.out.println();
        }

        // 传递闭包
        System.out.println("\n=== 传递闭包(可达性矩阵)===");
        boolean[][] closure = fw.transitiveClosure();
        for (int i = 0; i < fw.vertices; i++) {
            for (int j = 0; j < fw.vertices; j++) {
                System.out.print(closure[i][j] ? "  1  " : "  0  ");
            }
            System.out.println();
        }
    }
}

传递闭包的扩展应用

Floyd算法的框架可以轻松扩展为求解传递闭包(Transitive Closure)。传递闭包回答的问题是:对于图中的任意两个顶点i和j,是否存在一条从i到j的路径?

将距离矩阵的加法替换为逻辑或(OR),取最小值替换为逻辑与(AND),即可得到Warshall算法(Floyd算法的布尔版本)。

与其他最短路径算法的对比

算法 适用场景 时间复杂度 支持负权 全源/单源
Dijkstra 非负权图 O((V+E)logV) 否 单源
Bellman-Ford 含负权图 O(VE) 是 单源
Floyd-Warshall 稠密图全源 O(V³) 是 全源
Johnson 稀疏图全源 O(V²logV + VE) 是 全源

对于稠密图(E ≈ V²),Floyd的O(V³)比运行V次Dijkstra的O(V³logV)更优。对于稀疏图,Johnson算法更优。

动态规划的本质理解

Floyd算法是动态规划的教科书级范例:

  1. 最优子结构:全局最优解包含子问题的最优解。i到j的最短路径,如果经过k,则i→k和k→j也分别是最短路径。
  2. 重叠子问题:大量子问题被重复计算,用表格存储避免重复。
  3. 无后效性:状态转移只依赖于更小规模的状态,与如何达到该状态无关。

k作为最外层循环的顺序至关重要。它保证了在计算dp[k][i][j]时,dp[k-1][i][k]和dp[k-1][k][j]已经计算完成,符合动态规划的依赖顺序。

实际应用场景

  1. 网络路由:计算路由器之间的最短路径,用于OSPF等协议
  2. 社交网络分析:计算用户之间的最短连接路径(六度分隔理论)
  3. 交通规划:计算城市间最短通行时间和换乘方案
  4. 供应链优化:多仓库间的最优配送路径

复杂度分析

  • 时间复杂度:O(V³),三重循环,每个操作O(1)
  • 空间复杂度:O(V²),存储距离矩阵和路径矩阵
  • 优化空间:如果只需要距离而不需要路径,可以省略next矩阵;如果图是稀疏的,使用邻接表加Johnson算法更优

常见错误与注意事项

  1. k循环必须在外层:如果k放在内层,会导致部分状态未更新就用于后续计算,结果错误
  2. INF的选择:应足够大但不能导致加法溢出。Integer.MAX_VALUE/2是安全的选择
  3. 负权环检测:运行完算法后检查dist[i][i]是否为负值
  4. 路径重构:需要额外维护next矩阵,不能仅凭距离矩阵推导路径

总结

Floyd-Warshall算法以极其简洁的三重循环实现了全源最短路径的计算,是动态规划思想的完美体现。其O(V³)的复杂度在稠密图上具有竞争力,且实现简单、易于扩展。理解Floyd算法不仅掌握了一个实用的图算法,更重要的是领会了如何将复杂问题分解为状态、定义状态转移、最终用填表的方式优雅地解决。