每日算法 — 使用java实现HLPP最大流:最高标号预流推进与间隙启发式优化

算法概述

最高标号预流推进算法(Highest Label Preflow-Push,简称HLPP)是求解网络最大流问题的高效算法,由Goldberg和Tarjan提出。与Edmonds-Karp的增广路思想和Dinic的分层图阻塞流不同,HLPP采用”预流”概念:允许节点在计算过程中暂时拥有超出流出量的流入量(称为盈余),通过不断将盈余向汇点推送,最终使所有中间节点盈余为零,此时预流即转化为合法的最大流。

核心原理

HLPP的核心机制包含三个关键概念:

  1. 高度函数(Height/Label):每个节点被赋予一个高度值,预流只能从高节点推送到低节点(高度差至少为1)。汇点高度固定为0,源点高度初始为节点数。

  2. 预流推送(Push):对于存在盈余的节点,若存在邻接边允许推送(仍有残余容量且终点高度低1),则将尽可能多的流推送过去。

  3. 重标号(Relabel):当节点有盈余但无法向任何邻居推送时,将其高度提升为所有可到达邻居的最小高度加1,以创造新的推送机会。

最高标号策略:每次优先处理高度最高的盈余节点,使得流尽可能快地接近汇点。

间隙启发式(Gap Heuristic):若某高度值没有节点占据(出现”间隙”),则所有高于该间隙的节点不可能再有流到达汇点,可直接将其高度提升至n+1以上,加速算法收敛。

Java完整实现

import java.util.*;

/**
 * HLPP(最高标号预流推进)最大流算法实现
 * 时间复杂度:O(V²·√E) 实际表现接近 O(V³)
 * 适用场景:稠密图、复杂网络流建模
 */
public class HLPPMaxFlow {

    // 边类
    static class Edge {
        int to, rev;      // 终点、反向边索引
        long cap;         // 残余容量

        Edge(int to, long cap, int rev) {
            this.to = to;
            this.cap = cap;
            this.rev = rev;
        }
    }

    private int n;                    // 节点数
    private int source, sink;         // 源点、汇点
    private List<Edge>[] graph;       // 邻接表
    private long[] excess;            // 每个节点的盈余
    private int[] height;             // 高度标号
    private int[] count;              // 各高度的节点数(间隙启发式用)
    private List<Integer>[] buckets;  // 按高度分桶的活跃节点
    private int maxHeight;            // 当前最大高度

    @SuppressWarnings("unchecked")
    public HLPPMaxFlow(int n, int source, int sink) {
        this.n = n;
        this.source = source;
        this.sink = sink;
        this.graph = new ArrayList[n];
        for (int i = 0; i < n; i++) graph[i] = new ArrayList<>();
        this.excess = new long[n];
        this.height = new int[n];
        this.count = new int[2 * n + 1];
        this.buckets = new ArrayList[2 * n + 1];
        for (int i = 0; i < buckets.length; i++) buckets[i] = new ArrayList<>();
        this.maxHeight = 0;
    }

    /**
     * 添加有向边
     */
    public void addEdge(int from, int to, long capacity) {
        Edge forward = new Edge(to, capacity, graph[to].size());
        Edge backward = new Edge(from, 0, graph[from].size());
        graph[from].add(forward);
        graph[to].add(backward);
    }

    /**
     * 从u向v推送流量
     */
    private void push(int u, Edge edge) {
        int v = edge.to;
        long delta = Math.min(excess[u], edge.cap);
        if (delta == 0 || height[u] != height[v] + 1) return;

        edge.cap -= delta;
        graph[v].get(edge.rev).cap += delta;
        excess[u] -= delta;
        excess[v] += delta;

        // 将v加入活跃节点桶
        if (v != source && v != sink && excess[v] == delta) {
            buckets[height[v]].add(v);
            maxHeight = Math.max(maxHeight, height[v]);
        }
    }

    /**
     * 重标号节点u
     */
    private void relabel(int u) {
        int minHeight = Integer.MAX_VALUE;
        for (Edge e : graph[u]) {
            if (e.cap > 0) {
                minHeight = Math.min(minHeight, height[e.to]);
            }
        }

        if (minHeight < Integer.MAX_VALUE) {
            count[height[u]]--;        // 间隙启发式:原高度计数减1
            height[u] = minHeight + 1; // 新高度
            count[height[u]]++;        // 新高度计数加1
            maxHeight = Math.max(maxHeight, height[u]);
        }
    }

    /**
     * 间隙启发式:当某高度没有节点时,高于该高度的节点无法再推送流至汇点
     */
    private void gapHeuristic(int emptyHeight) {
        for (int u = 0; u < n; u++) {
            if (u != source && height[u] > emptyHeight && height[u] <= n) {
                count[height[u]]--;
                height[u] = Math.max(height[u], n + 1);
                count[height[u]]++;
            }
        }
    }

    /**
     * 从源点进行初始推送(BFS设置高度后)
     */
    private void initialize() {
        // BFS从汇点反向设置初始高度
        Arrays.fill(height, n); // 默认高度为n(不可达)
        height[sink] = 0;

        Queue<Integer> queue = new LinkedList<>();
        queue.add(sink);

        while (!queue.isEmpty()) {
            int u = queue.poll();
            for (Edge e : graph[u]) {
                int v = e.to;
                // 反向边有容量表示原边有流量,即v可以到达u
                Edge rev = graph[v].get(e.rev);
                if (rev.cap > 0 && height[v] == n) {
                    height[v] = height[u] + 1;
                    queue.add(v);
                }
            }
        }

        height[source] = n; // 源点高度设为n

        // 统计各高度节点数
        for (int i = 0; i < n; i++) {
            if (height[i] <= n) {
                count[height[i]]++;
            }
        }

        // 从源点推送初始流量
        for (Edge e : graph[source]) {
            if (e.cap > 0) {
                long delta = e.cap;
                e.cap = 0;
                graph[e.to].get(e.rev).cap += delta;
                excess[source] -= delta;
                excess[e.to] += delta;
                if (e.to != sink) {
                    buckets[height[e.to]].add(e.to);
                    maxHeight = Math.max(maxHeight, height[e.to]);
                }
            }
        }
    }

    /**
     * 执行HLPP算法计算最大流
     */
    public long maxFlow() {
        initialize();

        // 处理活跃节点(最高标号优先)
        while (maxHeight > 0) {
            List<Integer> bucket = buckets[maxHeight];

            while (!bucket.isEmpty()) {
                int u = bucket.remove(bucket.size() - 1);
                if (excess[u] == 0) continue;

                boolean pushed = false;
                for (Edge e : graph[u]) {
                    if (e.cap > 0 && height[u] == height[e.to] + 1) {
                        push(u, e);
                        pushed = true;
                        if (excess[u] == 0) break;
                    }
                }

                if (excess[u] > 0) {
                    if (!pushed) {
                        int oldHeight = height[u];
                        relabel(u);

                        // 间隙启发式检查
                        if (oldHeight > 0 && oldHeight <= n && count[oldHeight] == 0) {
                            gapHeuristic(oldHeight);
                        }
                    }
                    if (height[u] < buckets.length) {
                        buckets[height[u]].add(u);
                        maxHeight = Math.max(maxHeight, height[u]);
                    }
                }
            }

            maxHeight--;
        }

        return excess[sink];
    }

    /**
     * 获取最小割中源点侧的节点集合
     */
    public List<Integer> getMinCutSourceSide() {
        boolean[] visited = new boolean[n];
        Queue<Integer> queue = new LinkedList<>();
        queue.add(source);
        visited[source] = true;

        while (!queue.isEmpty()) {
            int u = queue.poll();
            for (Edge e : graph[u]) {
                if (e.cap > 0 && !visited[e.to]) {
                    visited[e.to] = true;
                    queue.add(e.to);
                }
            }
        }

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

    public static void main(String[] args) {
        // 测试经典案例
        HLPPMaxFlow hlpp = new HLPPMaxFlow(6, 0, 5);
        hlpp.addEdge(0, 1, 16);
        hlpp.addEdge(0, 2, 13);
        hlpp.addEdge(1, 2, 10);
        hlpp.addEdge(1, 3, 12);
        hlpp.addEdge(2, 1, 4);
        hlpp.addEdge(2, 4, 14);
        hlpp.addEdge(3, 2, 9);
        hlpp.addEdge(3, 5, 20);
        hlpp.addEdge(4, 3, 7);
        hlpp.addEdge(4, 5, 4);

        long maxFlow = hlpp.maxFlow();
        System.out.println("最大流: " + maxFlow); // 期望 23
        System.out.println("源点侧最小割: " + hlpp.getMinCutSourceSide());

        // 二分图匹配测试
        HLPPMaxFlow bipartite = new HLPPMaxFlow(8, 0, 7);
        // 左侧节点 1-3, 右侧节点 4-6
        bipartite.addEdge(0, 1, 1);
        bipartite.addEdge(0, 2, 1);
        bipartite.addEdge(0, 3, 1);
        bipartite.addEdge(1, 4, 1);
        bipartite.addEdge(1, 5, 1);
        bipartite.addEdge(2, 4, 1);
        bipartite.addEdge(2, 6, 1);
        bipartite.addEdge(3, 5, 1);
        bipartite.addEdge(4, 7, 1);
        bipartite.addEdge(5, 7, 1);
        bipartite.addEdge(6, 7, 1);

        System.out.println("\n二分图最大匹配: " + bipartite.maxFlow()); // 期望 3
    }
}

复杂度分析

指标 复杂度 说明
时间复杂度 O(V²·√E) 理论界,实际接近 O(V³)
实际性能 O(V·E) ~ O(V³) 在稠密图上优于Dinic
空间复杂度 O(V + E) 邻接表存储
非饱和推送 O(V²·E) 每次推送至少使一个节点高度增加
重标号次数 O(V²) 每个节点最多重标号V次

应用场景

  • 二分图最大匹配:通过拆点建流网络求解
  • 多源多汇网络:超级源汇建模
  • 最小割问题:最大流最小割定理的直接应用
  • 图像分割:GrabCut等算法的图割能量最小化
  • 交通网络:道路通行能力规划与瓶颈分析

HLPP算法在稠密图和复杂网络流模型中展现出比Dinic更稳定的性能表现,是竞赛和工程中的高阶选择。