每日算法 — 使用java实现Edmonds-Karp最大流:BFS增广路与残量网络迭代

算法简介

Edmonds-Karp算法是Ford-Fulkerson方法的一种具体实现,其核心改进在于每次寻找增广路时使用BFS(广度优先搜索)而非DFS,从而保证了算法的时间复杂度上界。该算法用于求解网络流问题中的最大流:给定一个有向图,每条边有容量限制,求从源点到汇点能传输的最大流量。

核心思想

最大流问题的核心是残量网络(Residual Network)的概念:

  1. 残量容量:对于每条边 $(u, v)$,残量容量 $r(u, v) = c(u, v) – f(u, v)$,表示该边还能增加的流量
  2. 反向边:为了允许”撤销”之前的流量分配,引入反向边,其残量容量 $r(v, u) = f(u, v)$
  3. 增广路:残量网络中从源点到汇点的一条路径,路径上所有边的残量容量都大于0
  4. 瓶颈容量:增广路上最小的残量容量,即本次增广能增加的最大流量

Edmonds-Karp算法不断使用BFS在残量网络中寻找最短增广路,直到不存在增广路为止,此时得到的流即为最大流。

Java实现

import java.util.*;

/**
 * Edmonds-Karp最大流算法实现
 * 使用BFS在残量网络中寻找最短增广路
 * 
 * @author AlgorithmDaily
 */
public class EdmondsKarp {

    private int n;                  // 顶点数量
    private int source;             // 源点
    private int sink;               // 汇点
    private int[][] capacity;       // 容量矩阵
    private int[][] flow;           // 流量矩阵
    private int[] parent;           // BFS记录父节点

    public EdmondsKarp(int n, int source, int sink) {
        this.n = n;
        this.source = source;
        this.sink = sink;
        this.capacity = new int[n][n];
        this.flow = new int[n][n];
        this.parent = new int[n];
    }

    /**
     * 添加边及其容量
     * @param u 起点
     * @param v 终点
     * @param cap 容量
     */
    public void addEdge(int u, int v, int cap) {
        capacity[u][v] += cap;  // 支持重边
    }

    /**
     * 使用BFS在残量网络中寻找从源点到汇点的最短增广路
     * @return 增广路的瓶颈容量,如果不存在则返回0
     */
    private int bfs() {
        Arrays.fill(parent, -1);
        parent[source] = source;

        // 队列中存储顶点,同时记录到达该顶点的残量容量
        Queue<int[]> queue = new LinkedList<>();
        queue.offer(new int[]{source, Integer.MAX_VALUE});

        while (!queue.isEmpty()) {
            int[] curr = queue.poll();
            int u = curr[0];
            int currFlow = curr[1];

            for (int v = 0; v < n; v++) {
                //  parent[v] == -1 表示未访问,且残量容量大于0
                if (parent[v] == -1 && capacity[u][v] - flow[u][v] > 0) {
                    parent[v] = u;
                    int newFlow = Math.min(currFlow, capacity[u][v] - flow[u][v]);

                    if (v == sink) {
                        return newFlow;  // 找到汇点,返回瓶颈容量
                    }

                    queue.offer(new int[]{v, newFlow});
                }
            }
        }

        return 0;  // 不存在增广路
    }

    /**
     * 执行Edmonds-Karp算法计算最大流
     * @return 从源点到汇点的最大流量
     */
    public int maxFlow() {
        int totalFlow = 0;
        int augmentFlow;

        // 循环直到不存在增广路
        while ((augmentFlow = bfs()) > 0) {
            totalFlow += augmentFlow;

            // 回溯更新流量矩阵
            int v = sink;
            while (v != source) {
                int u = parent[v];
                flow[u][v] += augmentFlow;  // 正向边增加流量
                flow[v][u] -= augmentFlow;  // 反向边减少流量(增加残量)
                v = u;
            }
        }

        return totalFlow;
    }

    /**
     * 获取最小割的S集合(源点一侧的顶点)
     */
    public List<Integer> getMinCutS() {
        boolean[] visited = new boolean[n];
        Queue<Integer> queue = new LinkedList<>();
        queue.offer(source);
        visited[source] = true;

        while (!queue.isEmpty()) {
            int u = queue.poll();
            for (int v = 0; v < n; v++) {
                if (!visited[v] && capacity[u][v] - flow[u][v] > 0) {
                    visited[v] = true;
                    queue.offer(v);
                }
            }
        }

        List<Integer> sSet = new ArrayList<>();
        for (int i = 0; i < n; i++) {
            if (visited[i]) sSet.add(i);
        }
        return sSet;
    }

    /**
     * 打印当前流量分配情况
     */
    public void printFlow() {
        System.out.println("=== 流量分配 ===");
        for (int u = 0; u < n; u++) {
            for (int v = 0; v < n; v++) {
                if (flow[u][v] > 0) {
                    System.out.printf("  %d -> %d: %d / %d%n", 
                        u, v, flow[u][v], capacity[u][v]);
                }
            }
        }
    }

    /**
     * 主程序:演示Edmonds-Karp算法
     */
    public static void main(String[] args) {
        // 构建测试网络(经典示例)
        //      0(源点)
        //     / | \
        //    1  2  3
        //   /\  |  /\
        //  4   5-6   7
        //   \  |  /  /
        //    \ | /  /
        //      8(汇点)

        EdmondsKarp ek = new EdmondsKarp(9, 0, 8);

        // 从源点出发的边
        ek.addEdge(0, 1, 10);
        ek.addEdge(0, 2, 10);
        ek.addEdge(0, 3, 10);

        // 中间层边
        ek.addEdge(1, 4, 5);
        ek.addEdge(1, 5, 5);
        ek.addEdge(2, 5, 10);
        ek.addEdge(3, 6, 5);
        ek.addEdge(3, 7, 5);

        // 交叉边
        ek.addEdge(5, 6, 10);

        // 到汇点的边
        ek.addEdge(4, 8, 5);
        ek.addEdge(5, 8, 10);
        ek.addEdge(6, 8, 5);
        ek.addEdge(7, 8, 5);

        int maxFlow = ek.maxFlow();
        System.out.println("=== Edmonds-Karp最大流 ===");
        System.out.println("最大流量: " + maxFlow);

        ek.printFlow();

        System.out.println("\n最小割S集合: " + ek.getMinCutS());

        // 验证:根据最大流最小割定理,最大流等于最小割容量
        System.out.println("\n验证:最大流 = " + maxFlow + ",应等于最小割容量");
    }
}

复杂度分析

  • 时间复杂度:$O(V \cdot E^2)$,其中 $V$ 是顶点数,$E$ 是边数。每次BFS的时间复杂度为 $O(E)$,而增广次数最多为 $O(V \cdot E)$。
  • 空间复杂度:$O(V^2)$,使用邻接矩阵存储容量和流量。若使用邻接表可优化到 $O(V + E)$。

算法特点与适用场景

优点:
– 时间复杂度有确定上界,相比Ford-Fulkerson的DFS实现更加稳定
– 实现简单,BFS天然保证了找到的是最短增广路
– 基于最大流最小割定理,算法正确性有严格证明

缺点:
– 时间复杂度不如Dinic算法($O(V^2 \cdot E)$ 或 $O(E \cdot \sqrt{V})$)
– 对于单位容量网络仍有优化空间

适用场景:网络路由优化、二分图最大匹配、任务分配、图像分割中的图割算法等。