土法炼钢兴趣小组的算法知识备份

网络流与二分匹配

目录

网络流理论是组合优化中最富有魅力的分支之一。它将现实世界中”物流在管道中流动”这一直觉, 抽象为精确的数学模型,并在此基础上导出了一系列深刻的对偶定理和高效算法。 从航线调度到图像分割,从棒球淘汰赛判定到编译器寄存器分配,网络流的应用远比多数人想象的广泛。

本文将从最大流问题的形式化定义出发,逐步展开 Ford-Fulkerson 方法、Edmonds-Karp 算法、 Dinic 算法,然后过渡到最大流最小割定理的证明,再进入二分匹配的世界—— 包括通过最大流归约、匈牙利算法和 Hopcroft-Karp 算法。 最后我们讨论最小费用最大流、真实工程案例,以及一份完整的 Dinic 算法 C 语言实现。

一、最大流问题的形式化定义

流网络

一个流网络(flow network)是一个有向图 G = (V, E),其中:

可行流

一个可行流(feasible flow)是函数 f: V x V -> R,满足以下两个约束:

  1. 容量约束:对所有 u, v,有 0 <= f(u, v) <= c(u, v)。
  2. 流量守恒:对所有 u(u 不为 s 和 t),流入等于流出:
sum_{v} f(v, u) = sum_{v} f(u, v)

流值

流的(value)定义为从源点净流出的总量:

|f| = sum_{v} f(s, v) - sum_{v} f(v, s)

最大流问题:在满足容量约束和流量守恒的前提下,求使 |f| 最大的可行流。

线性规划视角

最大流问题可以自然地表述为一个线性规划(LP):

maximize    sum_{v} f(s,v)
subject to  f(u,v) <= c(u,v)       对所有边 (u,v)
            f(u,v) >= 0             对所有边 (u,v)
            sum_v f(v,u) = sum_v f(u,v)  对所有 u != s, t

这个 LP 的对偶恰好给出最小割问题——我们稍后在第五节证明这一点。

二、Ford-Fulkerson 方法与增广路径

残量网络

给定流 f,残量网络 G_f = (V, E_f) 定义如下:

残量网络的核心思想是:前向边表示”还能再推多少”,反向边表示”已经推了多少可以撤回”。

增广路径

在残量网络 G_f 中从 s 到 t 的任意路径称为增广路径(augmenting path)。 沿增广路径能推送的最大流量为路径上所有边残量的最小值——这就是该路径的瓶颈容量

算法框架

Ford-Fulkerson(G, s, t):
    初始化 f(u,v) = 0  对所有 (u,v)
    while 残量网络 G_f 中存在从 s 到 t 的增广路径 p:
        delta = min{ c_f(u,v) : (u,v) in p }
        对 p 上每条边 (u,v):
            if (u,v) 是前向边:
                f(u,v) += delta
            else:
                f(v,u) -= delta
    return f

终止性与复杂度

一个经典陷阱

考虑如下简单图:s -> a, s -> b, a -> b, a -> t, b -> t, 其中 s->a 容量 1000,s->b 容量 1000,a->b 容量 1,a->t 容量 1000,b->t 容量 1000。

如果选择增广路径的策略不当(每次都经过 a->b 那条容量为 1 的边), 则需要 2000 次增广才能找到最大流 2000。这说明 Ford-Fulkerson 的效率严重依赖增广路径的选择策略。

三、Edmonds-Karp 算法:BFS 增广

核心改进

Edmonds 和 Karp(1972)提出了一个简单而深刻的改进: 每次选择残量网络中最短的增广路径——即用 BFS 寻找增广路径。

时间复杂度

Edmonds-Karp 算法的时间复杂度为 O(V * E^2),与最大流值无关。

证明关键引理

  1. 距离单调性:设 d_f(v) 为残量网络 G_f 中 s 到 v 的 BFS 距离。 每次增广后,对所有 v 有 d_{f’}(v) >= d_f(v)。

  2. 增广次数上界:每条边至多成为 O(V) 次增广的瓶颈边。 因为边 (u, v) 成为瓶颈边后被删除,要重新出现需要反向边 (v, u) 被使用, 此时 d(u) 至少增加 2。由于距离至多为 V,每条边至多贡献 O(V) 次瓶颈。

  3. 边数为 E,故总增广次数为 O(V * E)。每次 BFS 为 O(E),总计 O(V * E^2)。

伪代码

Edmonds-Karp(G, s, t):
    初始化 f(u,v) = 0
    while true:
        // BFS 寻找最短增广路径
        parent = BFS(G_f, s, t)
        if t 不可达:
            break
        // 计算瓶颈
        delta = INF
        v = t
        while v != s:
            u = parent[v]
            delta = min(delta, c_f(u, v))
            v = u
        // 更新流
        v = t
        while v != s:
            u = parent[v]
            f(u,v) += delta
            f(v,u) -= delta
            v = u
    return f

实践表现

虽然 O(V * E^2) 看起来不够快,Edmonds-Karp 在随机图和稀疏图上的表现往往远好于最坏情况。 对于竞赛中的小规模问题(V <= 500,E <= 5000),Edmonds-Karp 通常足够。

四、Dinic 算法:分层图与阻塞流

Dinic(1970)提出的算法是实践中最常用的最大流算法之一, 其核心思想是分层图(layered graph)和阻塞流(blocking flow)。

分层图的构造

对残量网络 G_f 做一次 BFS,计算每个节点到源点的距离 d(v)。 分层图 L_f 只保留满足 d(v) = d(u) + 1 的边 (u, v)——即只保留”沿最短路方向前进”的边。

下图展示了一个分层图的结构:

Dinic 分层图

图中节点按 BFS 距离分成四层(d=0, 1, 2, 3),边只从低层指向高层。 紫色虚线表示反向边,它们不属于分层图但存在于残量网络中。

阻塞流

在分层图 L_f 中,一个流 g 是阻塞流,当且仅当 L_f 中从 s 到 t 的每条路径 至少有一条边被 g 饱和(残量为 0)。

注意:阻塞流不一定是分层图中的最大流——它只要求阻断所有 s-t 路径即可。

算法流程

Dinic(G, s, t):
    初始化 f = 0
    while true:
        对 G_f 做 BFS,构造分层图 L_f
        if t 在 L_f 中不可达:
            break
        在 L_f 中找到一个阻塞流 g
        f = f + g    // 将阻塞流累加到总流
    return f

阻塞流的求解:DFS + 当前弧优化

在分层图上用 DFS 寻找阻塞流。关键优化是当前弧(current arc)技巧:

时间复杂度分析

定理:Dinic 算法的时间复杂度为 O(V^2 * E)。

证明

  1. 每次 BFS 阶段后,分层图的层数(即 d(t))严格递增。 因为阻塞流已阻断所有长度为 d(t) 的增广路径,下一轮 BFS 中 d(t) 至少增加 1。

  2. d(t) 至多为 V - 1,故 BFS 阶段至多执行 V - 1 次。

  3. 每个阶段中,阻塞流的计算需要 O(V * E) 时间。

  4. 总计 O(V * (V * E)) = O(V^2 * E)。

单位容量图上的特殊复杂度

当所有边容量为 1 时,Dinic 算法的复杂度降为 O(E * sqrt(V))。 这是因为:

这个性质使 Dinic 算法在二分匹配问题上能达到 O(E * sqrt(V))—— 与 Hopcroft-Karp 算法的复杂度相同。

五、最大流最小割定理

割的定义

给定流网络 G = (V, E),一个 s-t 割(cut)是将 V 划分为 S 和 T = V  S 的方式, 使得 s 属于 S,t 属于 T。割的容量为:

c(S, T) = sum_{u in S, v in T} c(u, v)

最小割问题:找使 c(S, T) 最小的 s-t 割。

定理陈述

最大流最小割定理(Ford & Fulkerson, 1956):

在任何流网络中,最大流的值等于最小割的容量。

这是组合优化中最深刻的对偶定理之一。

弱对偶性(容易的方向)

对于任意可行流 f 和任意 s-t 割 (S, T):

|f| = sum_{u in S, v in T} f(u,v) - sum_{u in T, v in S} f(u,v)
     <= sum_{u in S, v in T} f(u,v)
     <= sum_{u in S, v in T} c(u,v)
     = c(S, T)

因此:最大流值 <= 最小割容量。这是弱对偶性。

强对偶性(通过 Ford-Fulkerson 终止条件)

当 Ford-Fulkerson 算法终止时,残量网络中不存在从 s 到 t 的路径。 令 S = { v : v 在 G_f 中从 s 可达 },T = V  S。

由于 t 不在 S 中,(S, T) 是一个 s-t 割。对于此割:

因此:

|f| = sum_{u in S, v in T} c(u,v) - 0 = c(S, T)

我们找到了一个流和一个割,使流值等于割容量。 结合弱对偶性,这个流就是最大流,这个割就是最小割。

LP 对偶视角

最大流的 LP 对偶可以写成:

minimize    sum_{(u,v) in E} c(u,v) * d(u,v)
subject to  d(u,v) - p(u) + p(v) >= 0   对所有边 (u,v)
            p(s) - p(t) >= 1
            d(u,v) >= 0

其中 p(v) 是节点的”势”变量,d(u,v) 是边的变量。

当最优解中 p 只取 0/1 值时,{v : p(v) = 1} 恰好定义了一个最小割。 LP 强对偶定理保证最优值相等,这给出了最大流最小割定理的另一个证明。

这个对偶视角的美妙之处在于:它把一个组合优化定理(最大流 = 最小割) 还原为线性规划的一般性定理(LP 强对偶),揭示了问题的本质结构。

最小割的求解

最大流算法不仅给出最大流值,还自然地给出最小割: 算法终止后,在残量网络中从 s 做一次 BFS/DFS,可达节点集合为 S,不可达节点集合为 T。 (S, T) 就是一个最小割。

六、二分匹配与最大流归约

二分图匹配问题

给定二分图 G = (L, R, E),其中 L 和 R 是两个不相交的顶点集,E 的每条边连接 L 中的一个点和 R 中的一个点。 匹配是 E 的子集 M,使得每个顶点至多被 M 中的一条边覆盖。 最大匹配是边数最多的匹配。

通过最大流求解

构造流网络:

  1. 添加源点 s 和汇点 t。
  2. 对每个 l 属于 L,添加边 (s, l),容量为 1。
  3. 对每条二分图的边 (l, r),添加边 (l, r),容量为 1。
  4. 对每个 r 属于 R,添加边 (r, t),容量为 1。

定理:此流网络的最大流值等于原二分图的最大匹配数。

证明

由于 Dinic 算法在单位容量图上的复杂度为 O(E * sqrt(V)), 通过最大流求解二分匹配的时间复杂度为 O(E * sqrt(V))。

Hall 定理

Hall 定理(1935):二分图 G = (L, R, E) 存在覆盖 L 的完美匹配的充要条件是: 对 L 的任意子集 S,S 的邻居集 N(S) 满足 |N(S)| >= |S|。

Hall 定理可以由最大流最小割定理导出——这是网络流理论统一性的又一体现。

Konig 定理

Konig 定理(1931):在二分图中,最大匹配数等于最小顶点覆盖数。

这是最大流最小割定理在二分图上的特殊情形: 最大匹配对应最大流,最小顶点覆盖对应最小割。

七、匈牙利算法与加权匹配

匈牙利算法(Kuhn-Munkres)

匈牙利算法解决的是加权二分匹配问题:给定 n x n 的权重矩阵 W, 找一个完美匹配使总权重最大(或最小)。

算法思想

算法维护一组顶标(potential):对 L 中的每个点 u 有标签 lx[u], 对 R 中的每个点 v 有标签 ly[v],满足:

lx[u] + ly[v] >= W[u][v]    对所有 u, v

定义相等子图:只包含满足 lx[u] + ly[v] = W[u][v] 的边。

关键性质:如果相等子图中存在完美匹配,则该匹配就是最优匹配。 因为任何匹配 M 的权重满足:

sum_{(u,v) in M} W[u][v] <= sum_{u} lx[u] + sum_{v} ly[v]

而相等子图中的匹配恰好取等。

算法流程

Hungarian():
    // 初始化顶标
    lx[u] = max_v W[u][v]
    ly[v] = 0

    for 每个未匹配的 u in L:
        // 用 BFS 在相等子图中寻找增广路径
        if 找到增广路径:
            沿增广路径翻转匹配
        else:
            // 调整顶标,扩大相等子图
            delta = min{ lx[u] + ly[v] - W[u][v] :
                         u in 已访问的 L 节点,
                         v in 未访问的 R 节点 }
            对已访问的 L 节点: lx[u] -= delta
            对已访问的 R 节点: ly[v] += delta
            重新尝试增广

时间复杂度

朴素实现为 O(n^3),使用 Dijkstra 风格的顶标调整可优化到 O(n^3)—— 但常数因子更小。对于 n <= 1000 的问题,匈牙利算法是首选。

与最小费用流的关系

加权二分匹配等价于最小费用最大流:在二分匹配的流网络中, 将边权作为费用,容量保持为 1。匈牙利算法可以视为在这个特殊结构上 的最小费用流算法的专门化版本。

八、Hopcroft-Karp 算法

动机

朴素的增广路径匹配算法(每次 DFS/BFS 找一条增广路径)的复杂度为 O(V * E)。 Hopcroft 和 Karp(1973)发现:同时沿所有最短增广路径增广可以大幅减少阶段数。

算法框架

Hopcroft-Karp(G):
    M = 空匹配
    while true:
        // 阶段 1:BFS 找所有最短增广路径的长度
        用 BFS 从所有未匹配的 L 节点出发,
        交替经过未匹配边和匹配边,
        直到到达某个未匹配的 R 节点。
        记录分层信息。

        if 无增广路径:
            break

        // 阶段 2:DFS 找极大组不相交的最短增广路径
        for 每个未匹配的 L 节点 u:
            DFS(u)  // 沿分层图寻找增广路径并翻转
    return M

时间复杂度

定理:Hopcroft-Karp 算法的时间复杂度为 O(sqrt(V) * E)。

证明思路

  1. 每个阶段执行一次 BFS(O(E))和若干次 DFS(总计 O(E)),故每个阶段 O(E)。

  2. 设当前匹配 M 的大小为 m,最大匹配大小为 m*。 在第 k 个阶段后,最短增广路径长度至少为 2k + 1。

  3. 若当前最短增广路径长度为 l,则 m* - m <= V / l。 因此当 l > sqrt(V) 时,剩余增广量不超过 sqrt(V), 每个阶段至少增广 1,故剩余阶段数不超过 sqrt(V)。

  4. 前 sqrt(V) 个阶段已执行,加上至多 sqrt(V) 个后续阶段, 总阶段数为 O(sqrt(V))。

  5. 总时间 O(sqrt(V) * E)。

与 Dinic 的关系

在单位容量网络上运行 Dinic 算法,其行为与 Hopcroft-Karp 算法本质相同:

这并非巧合——Hopcroft-Karp 可以视为 Dinic 算法在二分匹配上的特殊化。

九、最小费用最大流

问题定义

在流网络的基础上,每条边 (u, v) 除了容量 c(u, v) 外,还有单位费用 w(u, v)。 最小费用最大流问题要求:在所有最大流中,找总费用最小的那个。

总费用 = sum_{(u,v)} f(u,v) * w(u,v)

连续最短路算法(SSP)

最小费用最大流的经典算法是连续最短路算法(Successive Shortest Path):

SSP(G, s, t):
    f = 0
    while 残量网络 G_f 中存在从 s 到 t 的路径:
        在 G_f 中用最短路算法(以费用为边权)找 s 到 t 的最短路径 p
        沿 p 增广(增广量为路径上的最小残量)
        更新 f
    return f

处理负权边

残量网络中的反向边费用为 -w(u, v),这意味着图中可能存在负权边。 解决方案:

  1. Bellman-Ford:直接处理负权边,单次 O(V * E),总计 O(V^2 * E^2) 或 O(F * V * E)。
  2. Johnson 技术:用势函数消除负权边,然后用 Dijkstra。 先用一次 Bellman-Ford 计算势函数 h(v),之后每次增广用 Dijkstra, 单次 O(E log V),总计 O(F * E log V) 或 O(V * E * log V) 取决于场景。

SPFA 加速

在实践中,SPFA(Shortest Path Faster Algorithm)常被用于最小费用流: 它是 Bellman-Ford 的队列优化版本,平均复杂度通常远好于最坏情况。 但需注意:SPFA 在特殊构造图上可能退化到 O(V * E)。

Primal-Dual 方法

对于更一般的最小费用流问题,Primal-Dual 方法同时维护原始变量(流)和对偶变量(势), 通过对偶变量指导增广方向。这种方法在理论和实践中都很重要, 它是匈牙利算法和网络单纯形法的统一框架。

应用场景

最小费用最大流广泛应用于:

十、Dinic 算法的完整 C 实现

以下是 Dinic 算法的完整 C 语言实现,包含链式前向星存图、BFS 分层和 DFS 阻塞流。 代码经过充分注释,可直接用于竞赛或工程项目。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>

/*
 * Dinic 最大流算法
 *
 * 时间复杂度:O(V^2 * E)
 * 空间复杂度:O(V + E)
 *
 * 使用链式前向星(adjacency list with edge array)存图,
 * 正向边和反向边成对存储,便于残量网络维护。
 */

#define MAXN 10005
#define MAXM 200005
#define INF  0x3f3f3f3f

/* ---- 链式前向星 ---- */

struct Edge {
    int to;       /* 终点 */
    int cap;      /* 残量 */
    int next;     /* 同起点的下一条边 */
};

static struct Edge edges[MAXM * 2];
static int head[MAXN];
static int edge_cnt;

static void init_graph(int n)
{
    memset(head, -1, sizeof(int) * (n + 1));
    edge_cnt = 0;
}

/*
 * 添加一条从 u 到 v、容量为 cap 的边,
 * 同时添加容量为 0 的反向边。
 * 正向边编号为偶数,反向边编号为奇数——
 * 利用 XOR 1 可以快速定位配对边。
 */
static void add_edge(int u, int v, int cap)
{
    edges[edge_cnt].to   = v;
    edges[edge_cnt].cap  = cap;
    edges[edge_cnt].next = head[u];
    head[u] = edge_cnt++;

    edges[edge_cnt].to   = u;
    edges[edge_cnt].cap  = 0;     /* 反向边初始残量为 0 */
    edges[edge_cnt].next = head[v];
    head[v] = edge_cnt++;
}

/* ---- BFS 分层 ---- */

static int level[MAXN];
static int queue[MAXN];

/*
 * 在残量网络上做 BFS,计算每个节点到源点的层次。
 * 返回 1 表示汇点可达(存在增广路径),0 表示不可达。
 */
static int bfs(int s, int t, int n)
{
    memset(level, -1, sizeof(int) * (n + 1));
    int front = 0, back = 0;

    level[s] = 0;
    queue[back++] = s;

    while (front < back) {
        int u = queue[front++];
        for (int i = head[u]; i != -1; i = edges[i].next) {
            int v = edges[i].to;
            if (level[v] == -1 && edges[i].cap > 0) {
                level[v] = level[u] + 1;
                queue[back++] = v;
            }
        }
    }

    return level[t] != -1;
}

/* ---- DFS 寻找阻塞流 ---- */

static int cur[MAXN];   /* 当前弧优化 */

/*
 * 从 u 出发,在分层图上 DFS 推送最多 pushed 单位的流到汇点 t。
 * 返回实际推送的流量。
 *
 * cur[] 实现当前弧优化:已经探索过的死路不会再走,
 * 保证单次阻塞流的总时间为 O(VE)。
 */
static int dfs(int u, int t, int pushed)
{
    if (u == t)
        return pushed;

    for (int *i = &cur[u]; *i != -1; *i = edges[*i].next) {
        int e  = *i;
        int v  = edges[e].to;

        if (level[v] != level[u] + 1 || edges[e].cap <= 0)
            continue;

        int d = dfs(v, t, pushed < edges[e].cap ? pushed : edges[e].cap);

        if (d > 0) {
            edges[e].cap     -= d;   /* 正向边残量减少 */
            edges[e ^ 1].cap += d;   /* 反向边残量增加 */
            return d;
        }
    }

    return 0;
}

/* ---- Dinic 主函数 ---- */

/*
 * 求从 s 到 t 的最大流。
 * 调用前需先用 init_graph() 初始化,再用 add_edge() 添加所有边。
 */
static long long dinic(int s, int t, int n)
{
    long long max_flow = 0;

    while (bfs(s, t, n)) {
        /* 每轮 BFS 后重置当前弧 */
        memcpy(cur, head, sizeof(int) * (n + 1));

        int d;
        while ((d = dfs(s, t, INF)) > 0) {
            max_flow += d;
        }
    }

    return max_flow;
}

/* ---- 输入/输出与主函数 ---- */

/*
 * 输入格式:
 *   第一行:n m s t (节点数、边数、源点、汇点)
 *   接下来 m 行:u v cap (有向边 u->v,容量 cap)
 *
 * 输出格式:
 *   最大流值
 */
int main(void)
{
    int n, m, s, t;

    if (scanf("%d %d %d %d", &n, &m, &s, &t) != 4) {
        fprintf(stderr, "输入格式错误\n");
        return 1;
    }

    init_graph(n);

    for (int i = 0; i < m; i++) {
        int u, v, cap;
        if (scanf("%d %d %d", &u, &v, &cap) != 3) {
            fprintf(stderr, "边 %d 输入格式错误\n", i);
            return 1;
        }
        add_edge(u, v, cap);
    }

    printf("%lld\n", dinic(s, t, n));

    return 0;
}

/*
 * 使用示例:
 *
 * 输入(上图的网络):
 *   7 10 1 7
 *   1 2 10
 *   1 3 8
 *   1 4 7
 *   2 5 6
 *   2 6 4
 *   3 5 5
 *   3 6 3
 *   4 6 7
 *   5 7 9
 *   6 7 12
 *
 * 输出:
 *   21
 *
 * 编译:gcc -O2 -o dinic dinic.c
 * 运行:./dinic < input.txt
 */

实现要点

  1. 链式前向星:用数组模拟邻接表,避免 malloc 开销。 正向边和反向边成对存储,编号分别为 2k 和 2k+1, 通过 XOR 1 即可找到配对边——这是竞赛中的经典技巧。

  2. 当前弧优化cur[] 数组在每轮 BFS 后重置为 head[], DFS 过程中只向前推进不回退。这保证每条边在一轮 DFS 中至多被检查常数次。

  3. 流量类型:主函数使用 long long 累加最大流值,防止溢出。 DFS 内部使用 int,因为单次增广量不会超过 INT_MAX(假设边容量在 int 范围内)。

  4. 静态数组:全局静态数组避免了动态内存分配, 适合竞赛和嵌入式场景。实际工程中可改为动态分配。

十一、实际应用与工程经验

航线调度

航空公司需要用最少数量的飞机覆盖一组航班。 每个航班有固定的起飞时间、降落时间和机场。 如果航班 A 降落后,飞机有足够时间转场或直接执飞航班 B, 则 A 和 B 之间可以衔接。

建模:构造二分图,左侧为航班的”出发端”,右侧为航班的”到达端”。 若 A 可以衔接 B,则连边。最小飞机数 = 航班总数 - 最大匹配数。

图像分割

计算机视觉中的前景/背景分割可以建模为最小割问题:

最小割将像素分为两组,最小化分割的总代价。 这就是 Boykov-Kolmogorov 算法的基础,广泛应用于医学影像和视频编辑。

棒球淘汰赛判定

给定棒球联赛的当前积分榜和剩余赛程,判断某支队伍是否已被淘汰(不可能夺冠)。 这个看似简单的问题,其精确判定需要用到最大流:

如果最大流等于所有剩余比赛的总场次,则目标队伍仍有机会;否则已被淘汰。

工程实践中的注意事项

问题 根因 解决方案
整数溢出 流值累加超出 int 范围 使用 long long 或 int64_t
重边处理 同一对节点间有多条边 链式前向星天然支持重边,无需合并
无向图建模 无向边等价于两条有向边 添加 (u,v) 和 (v,u) 各容量 c,但共享反向边时需注意
浮点容量 精度丢失导致死循环 乘以公倍数转为整数,或使用 epsilon 比较
超级源/汇 多源多汇问题 添加超级源 S 和超级汇 T,S 到每个源连无穷边,每个汇到 T 连无穷边
节点容量 节点有流量上限 拆点:将节点 v 拆为 v_in 和 v_out,中间连容量为上限的边
下界可行流 边有流量下界 转化为无下界问题:减去下界,修正源汇容量,添加辅助边
负环检测 最小费用流中出现负环 用 Bellman-Ford 检测,或使用势函数消除
稀疏 vs 稠密 算法选择不当 稀疏图用 Dinic,稠密图考虑 Push-Relabel
TLE 调试困难 不清楚瓶颈在哪 先确认图的规模和算法复杂度,再检查实现常数

算法选型指南

场景 推荐算法 复杂度
一般最大流(V <= 10^4) Dinic O(V^2 E)
稠密图最大流 Push-Relabel (HLPQ) O(V^2 sqrt(E))
二分匹配(无权) Hopcroft-Karp / Dinic O(E sqrt(V))
二分匹配(带权) 匈牙利 / KM O(V^3)
最小费用最大流 SPFA 增广 / Primal-Dual O(VE * F) / O(VE log V * F)
最小割(图像分割) Boykov-Kolmogorov 实践中远优于理论界

十二、个人思考与总结

网络流理论的美

网络流理论的美在于对偶性的无处不在

理解网络流,关键不在于背诵某个算法的实现细节,而在于理解残量网络这一核心抽象。 一旦你理解了”前向边表示剩余容量,反向边表示已推送流量”这一思想, 所有最大流算法都只是在残量网络上寻找增广路径的不同策略。

学习路径建议

我建议的学习路径是:

  1. 先理解 Ford-Fulkerson 方法和残量网络的概念。
  2. 实现 Edmonds-Karp(BFS 增广),在小图上手动模拟。
  3. 学习 Dinic 算法,重点理解分层图和当前弧优化。
  4. 证明最大流最小割定理——这是检验你是否真正理解的试金石。
  5. 学习二分匹配的最大流归约。
  6. 如有需要,再学习最小费用流和匈牙利算法。

竞赛中的经验

在算法竞赛中,网络流题目的难点通常不在于实现算法本身, 而在于建模——如何将问题转化为网络流问题。一些常见的建模技巧:

我个人的体会是:写过十道网络流建模题后,你会对这类问题形成直觉。 前三道可能完全没有思路,到第十道时你就能在十分钟内完成建模。 这是一种需要练习才能获得的能力,光读理论是不够的。

工程中的考量

在工程项目中使用网络流算法时,需要注意:

最后的话

网络流是我在算法学习过程中最享受的主题之一。 它将图论、线性规划和组合优化串联在一起,展示了数学的统一性和力量。 每当我在一个看似无关的问题中发现网络流的影子时, 都会由衷地感叹这个理论的深远与优雅。

希望这篇文章能帮助你走进网络流的世界。 如果你觉得某些部分不够清晰,欢迎回头多读几遍—— 网络流的核心概念并不多,但每一个都值得反复咀嚼。

参考文献

  1. Ford, L. R., Fulkerson, D. R. (1956). “Maximal Flow through a Network.” Canadian Journal of Mathematics, 8, 399-404.

  2. Edmonds, J., Karp, R. M. (1972). “Theoretical Improvements in Algorithmic Efficiency for Network Flow Problems.” Journal of the ACM, 19(2), 248-264.

  3. Dinic, E. A. (1970). “Algorithm for Solution of a Problem of Maximum Flow in Networks with Power Estimation.” Soviet Mathematics Doklady, 11, 1277-1280.

  4. Hopcroft, J. E., Karp, R. M. (1973). “An n^{5/2} Algorithm for Maximum Matchings in Bipartite Graphs.” SIAM Journal on Computing, 2(4), 225-231.

  5. Kuhn, H. W. (1955). “The Hungarian Method for the Assignment Problem.” Naval Research Logistics Quarterly, 2, 83-97.

  6. Boykov, Y., Kolmogorov, V. (2004). “An Experimental Comparison of Min-Cut/Max-Flow Algorithms for Energy Minimization in Vision.” IEEE Transactions on Pattern Analysis and Machine Intelligence, 26(9), 1124-1137.

  7. Korte, B., Vygen, J. (2018). Combinatorial Optimization: Theory and Algorithms. 6th edition, Springer.

  8. Ahuja, R. K., Magnanti, T. L., Orlin, J. B. (1993). Network Flows: Theory, Algorithms, and Applications. Prentice Hall.


上一篇: Tarjan 算法族

下一篇: 拓扑排序

相关阅读: - 最小生成树 - 图着色与寄存器分配


By .