3560 字
18 分钟
网络流与二分图匹配:从CSP-S到IOI

网络流与二分图匹配:从CSP-S到IOI#

网络流是图论中最重要的算法之一,广泛应用于资源分配、任务调度、匹配问题等场景。本文将系统介绍从入门到进阶的网络流算法体系。

一、网络流基本概念#

1.1 流网络定义#

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

  • 每条边 (u,v)E(u,v) \in E 有一个非负容量 c(u,v)0c(u,v) \geq 0
  • 存在源点 ss 和汇点 tt,其中 ss 没有入边,tt 没有出边
  • 对于不存在的边 (u,v)E(u,v) \notin E,规定 c(u,v)=0c(u,v) = 0

1.2 流的性质#

ff 是定义在边集上的函数,满足:

容量限制(u,v)E,0f(u,v)c(u,v)\forall (u,v) \in E, 0 \leq f(u,v) \leq c(u,v)

流量守恒uV{s,t},vVf(v,u)=vVf(u,v)\forall u \in V - \{s,t\}, \sum_{v \in V} f(v,u) = \sum_{v \in V} f(u,v)

流的值定义为:f=vVf(s,v)vVf(v,s)|f| = \sum_{v \in V} f(s,v) - \sum_{v \in V} f(v,s)

1.3 残量网络与增广路#

残量网络 GfG_f 中,每条边的残量容量为: cf(u,v)=c(u,v)f(u,v)c_f(u,v) = c(u,v) - f(u,v)

增广路是残量网络中从 sstt 的一条路径,沿途所有边的残量容量都大于0。

增广路定理:流 ff 是最大流当且仅当残量网络 GfG_f 中不存在增广路。

二、最大流算法#

2.1 Ford-Fulkerson 方法#

Ford-Fulkerson 是一种框架性方法,核心思想是不断寻找增广路并增广。

算法流程

  1. 初始化流 ff 为 0
  2. 当存在增广路 pp 时:
    • 计算 pp 的残量容量 cf(p)=min{cf(u,v):(u,v)p}c_f(p) = \min\{c_f(u,v) : (u,v) \in p\}
    • 沿 pp 增广流量 cf(p)c_f(p)
  3. 返回流 ff

时间复杂度O(Ef)O(E \cdot |f^*|),其中 f|f^*| 是最大流的值。

2.2 Edmonds-Karp 算法#

使用BFS寻找最短增广路的Ford-Fulkerson实现。

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1005;
const int INF = 1e9;
struct Edge {
int to, cap, rev;
};
vector<Edge> graph[MAXN];
int level[MAXN], iter[MAXN];
int n, m, s, t;
void add_edge(int from, int to, int cap) {
graph[from].push_back({to, cap, (int)graph[to].size()});
graph[to].push_back({from, 0, (int)graph[from].size() - 1});
}
bool bfs() {
memset(level, -1, sizeof(level));
queue<int> q;
q.push(s);
level[s] = 0;
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto& e : graph[u]) {
if (e.cap > 0 && level[e.to] < 0) {
level[e.to] = level[u] + 1;
q.push(e.to);
}
}
}
return level[t] >= 0;
}
int dfs(int u, int pushed) {
if (u == t || pushed == 0) return pushed;
for (int& i = iter[u]; i < graph[u].size(); i++) {
Edge& e = graph[u][i];
if (level[u] + 1 != level[e.to] || e.cap <= 0)
continue;
int flow = dfs(e.to, min(pushed, e.cap));
if (flow > 0) {
e.cap -= flow;
graph[e.to][e.rev].cap += flow;
return flow;
}
}
return 0;
}
int edmonds_karp() {
int max_flow = 0;
while (bfs()) {
memset(iter, 0, sizeof(iter));
int flow;
while ((flow = dfs(s, INF)) > 0) {
max_flow += flow;
}
}
return max_flow;
}

时间复杂度O(VE2)O(VE^2)

2.3 Dinic 算法#

Dinic 算法通过构造分层图和多路增广优化效率。

关键优化

  1. 分层图:使用BFS构造按最短路长度分层的图
  2. 当前弧优化:记录每个节点当前搜索到的边,避免重复搜索
  3. 多路增广:一次BFS后可以进行多次DFS增广
// Dinic算法实现(使用上面的代码结构)
int dinic() {
int max_flow = 0;
while (bfs()) {
memset(iter, 0, sizeof(iter));
int flow;
while ((flow = dfs(s, INF)) > 0) {
max_flow += flow;
}
}
return max_flow;
}

时间复杂度O(V2E)O(V^2E),单位容量网络中为 O(EV)O(E\sqrt{V})

2.4 ISAP 算法#

ISAP (Improved Shortest Augmenting Path) 通过维护距离标号和gap优化避免无效BFS。

int gap[MAXN], dis[MAXN], cur[MAXN];
int cnt[MAXN];
void init_isap() {
memset(dis, 0, sizeof(dis));
memset(gap, 0, sizeof(gap));
memset(cnt, 0, sizeof(cnt));
queue<int> q;
q.push(t);
dis[t] = 0;
cnt[0] = 1;
while (!q.empty()) {
int u = q.front();
q.pop();
for (auto& e : graph[u]) {
int v = e.to;
if (dis[v] == 0 && v != t) {
dis[v] = dis[u] + 1;
cnt[dis[v]]++;
q.push(v);
}
}
}
}
int isap_dfs(int u, int pushed) {
if (u == t) return pushed;
int flow = 0;
for (int& i = cur[u]; i < graph[u].size(); i++) {
Edge& e = graph[u][i];
if (e.cap > 0 && dis[u] == dis[e.to] + 1) {
int f = isap_dfs(e.to, min(pushed - flow, e.cap));
if (f > 0) {
e.cap -= f;
graph[e.to][e.rev].cap += f;
flow += f;
if (flow == pushed) return flow;
}
}
}
// Gap优化
if (--cnt[dis[u]] == 0) dis[s] = n + 1;
cnt[++dis[u]]++;
cur[u] = 0;
return flow;
}
int isap() {
init_isap();
int max_flow = 0;
while (dis[s] < n) {
memcpy(cur, iter, sizeof(cur));
max_flow += isap_dfs(s, INF);
}
return max_flow;
}

时间复杂度O(V2E)O(V^2E)

三、最小费用最大流#

最小费用最大流问题在最大流的基础上,每条边增加单位流量费用 w(u,v)w(u,v),目标是在流量最大的前提下使总费用最小。

3.1 SPFA 增广#

使用SPFA算法在残量网络中寻找最小费用增广路。

const int MAXN = 5005;
const int INF = 1e9;
struct Edge {
int to, cap, cost, rev;
};
vector<Edge> graph[MAXN];
int dist[MAXN], pre[MAXN], pre_edge[MAXN];
bool inq[MAXN];
int n, s, t;
void add_edge_cost(int from, int to, int cap, int cost) {
graph[from].push_back({to, cap, cost, (int)graph[to].size()});
graph[to].push_back({from, 0, -cost, (int)graph[from].size() - 1});
}
bool spfa() {
fill(dist, dist + n + 1, INF);
memset(inq, 0, sizeof(inq));
queue<int> q;
q.push(s);
dist[s] = 0;
inq[s] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
inq[u] = false;
for (int i = 0; i < graph[u].size(); i++) {
Edge& e = graph[u][i];
if (e.cap > 0 && dist[e.to] > dist[u] + e.cost) {
dist[e.to] = dist[u] + e.cost;
pre[e.to] = u;
pre_edge[e.to] = i;
if (!inq[e.to]) {
q.push(e.to);
inq[e.to] = true;
}
}
}
}
return dist[t] != INF;
}
pair<int, int> min_cost_max_flow() {
int max_flow = 0, min_cost = 0;
while (spfa()) {
int flow = INF;
// 找到增广路上的最小残量
for (int v = t; v != s; v = pre[v]) {
int u = pre[v];
flow = min(flow, graph[u][pre_edge[v]].cap);
}
// 更新流量和费用
for (int v = t; v != s; v = pre[v]) {
int u = pre[v];
int i = pre_edge[v];
graph[u][i].cap -= flow;
graph[v][graph[u][i].rev].cap += flow;
}
max_flow += flow;
min_cost += flow * dist[t];
}
return {max_flow, min_cost};
}

时间复杂度O(nmf)O(nmf),其中 ff 是最大流

3.2 势函数优化的Dijkstra#

使用势函数 h(v)h(v) 将边权变为非负,从而使用Dijkstra加速。

int h[MAXN];
void init_potential() {
fill(h, h + n + 1, INF);
h[s] = 0;
queue<int> q;
q.push(s);
bool inq[MAXN] = {0};
inq[s] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
inq[u] = false;
for (auto& e : graph[u]) {
if (e.cap > 0 && h[e.to] > h[u] + e.cost) {
h[e.to] = h[u] + e.cost;
if (!inq[e.to]) {
q.push(e.to);
inq[e.to] = true;
}
}
}
}
}
bool dijkstra() {
priority_queue<pair<int,int>, vector<pair<int,int>>, greater<>> pq;
fill(dist, dist + n + 1, INF);
dist[s] = 0;
pq.push({0, s});
while (!pq.empty()) {
auto [d, u] = pq.top();
pq.pop();
if (d > dist[u]) continue;
for (int i = 0; i < graph[u].size(); i++) {
Edge& e = graph[u][i];
int cost = e.cost + h[u] - h[e.to];
if (e.cap > 0 && dist[e.to] > dist[u] + cost) {
dist[e.to] = dist[u] + cost;
pre[e.to] = u;
pre_edge[e.to] = i;
pq.push({dist[e.to], e.to});
}
}
}
if (dist[t] == INF) return false;
for (int i = 0; i <= n; i++) {
if (dist[i] != INF) h[i] += dist[i];
}
return true;
}

时间复杂度O(fElogV)O(fE\log V)

四、二分图匹配#

4.1 匈牙利算法#

二分图最大匹配的经典算法,基于交替路和增广路思想。

const int MAXN = 1005;
vector<int> g[MAXN];
int match[MAXN];
bool vis[MAXN];
int n, m;
bool dfs(int u) {
for (int v : g[u]) {
if (!vis[v]) {
vis[v] = true;
if (match[v] == -1 || dfs(match[v])) {
match[v] = u;
return true;
}
}
}
return false;
}
int hungarian() {
memset(match, -1, sizeof(match));
int ans = 0;
for (int i = 1; i <= n; i++) {
memset(vis, 0, sizeof(vis));
if (dfs(i)) ans++;
}
return ans;
}

时间复杂度O(VE)O(VE)

4.2 Kuhn-Munkres (KM) 算法#

用于求解带权二分图的最大权完美匹配。

const int MAXN = 305;
const int INF = 1e9;
int w[MAXN][MAXN];
int lx[MAXN], ly[MAXN];
int match[MAXN];
bool visx[MAXN], visy[MAXN];
int slack[MAXN];
int n;
bool dfs_km(int u) {
visx[u] = true;
for (int v = 1; v <= n; v++) {
if (visy[v]) continue;
int gap = lx[u] + ly[v] - w[u][v];
if (gap == 0) {
visy[v] = true;
if (match[v] == -1 || dfs_km(match[v])) {
match[v] = u;
return true;
}
} else {
slack[v] = min(slack[v], gap);
}
}
return false;
}
int kuhn_munkres() {
memset(match, -1, sizeof(match));
memset(ly, 0, sizeof(ly));
for (int i = 1; i <= n; i++) {
lx[i] = -INF;
for (int j = 1; j <= n; j++) {
lx[i] = max(lx[i], w[i][j]);
}
}
for (int i = 1; i <= n; i++) {
fill(slack + 1, slack + n + 1, INF);
while (true) {
memset(visx, 0, sizeof(visx));
memset(visy, 0, sizeof(visy));
if (dfs_km(i)) break;
int delta = INF;
for (int j = 1; j <= n; j++) {
if (!visy[j]) delta = min(delta, slack[j]);
}
for (int j = 1; j <= n; j++) {
if (visx[j]) lx[j] -= delta;
if (visy[j]) ly[j] += delta;
else slack[j] -= delta;
}
}
}
int ans = 0;
for (int i = 1; i <= n; i++) {
if (match[i] != -1) {
ans += w[match[i]][i];
}
}
return ans;
}

时间复杂度O(V3)O(V^3)

五、最小割与最大流定理#

5.1 最大流最小割定理#

定理:网络中最大流的值等于最小割的容量。

(S,T)(S,T) 是将顶点集 VV 划分为两个集合,其中 sS,tTs \in S, t \in T

割的容量:c(S,T)=uS,vTc(u,v)c(S,T) = \sum_{u \in S, v \in T} c(u,v)

证明思路

  1. 任何流的值 \leq 任何割的容量
  2. 找到一个流和一个割使得它们相等

5.2 最小割应用#

求最小割只需在最大流算法结束后,在残量网络中从源点BFS能到达的点属于 SS,其余属于 TT

void find_min_cut(vector<int>& S, vector<int>& T) {
bool vis[MAXN] = {0};
queue<int> q;
q.push(s);
vis[s] = true;
while (!q.empty()) {
int u = q.front();
q.pop();
S.push_back(u);
for (auto& e : graph[u]) {
if (e.cap > 0 && !vis[e.to]) {
vis[e.to] = true;
q.push(e.to);
}
}
}
for (int i = 1; i <= n; i++) {
if (!vis[i]) T.push_back(i);
}
}

六、网络流建模技巧#

6.1 拆点技巧#

点容量限制:将点 vv 拆成 vinv_{in}voutv_{out},连边 (vin,vout,cv)(v_{in}, v_{out}, c_v)

6.2 多源多汇#

建立超级源点 ss' 和超级汇点 tt'

  • ss' 向所有源点连容量为 \infty 的边
  • 从所有汇点向 tt' 连容量为 \infty 的边

6.3 最大权闭合子图#

问题:给定带权有向图,求权值和最大的闭合子图。

建模

  • 源点向所有正权点连边,容量为权值
  • 所有负权点向汇点连边,容量为权值的绝对值
  • 原图边容量为 \infty
  • 答案 = 正权和 - 最小割
int max_closure(vector<int>& weight, vector<pair<int,int>>& edges) {
int sum_pos = 0;
for (int i = 0; i < weight.size(); i++) {
if (weight[i] > 0) {
add_edge(s, i + 1, weight[i]);
sum_pos += weight[i];
} else if (weight[i] < 0) {
add_edge(i + 1, t, -weight[i]);
}
}
for (auto [u, v] : edges) {
add_edge(u + 1, v + 1, INF);
}
return sum_pos - max_flow();
}

七、上下界网络流#

7.1 无源汇上下界可行流#

每条边 (u,v)(u,v) 有流量下界 l(u,v)l(u,v) 和上界 r(u,v)r(u,v)

转化方法

  1. 对每条边 (u,v)(u,v),强制流量至少为 l(u,v)l(u,v)
  2. 建立新图,边容量为 r(u,v)l(u,v)r(u,v) - l(u,v)
  3. 对每个点维护初始盈亏度:M(v)=ul(u,v)wl(v,w)M(v) = \sum_{u} l(u,v) - \sum_{w} l(v,w)
  4. 建立超级源汇,M(v)>0M(v) > 0 时从源点连边,M(v)<0M(v) < 0 时连向汇点
  5. 求最大流,若流量等于所有从源点出发的边容量和,则存在可行流
int lower[MAXN][MAXN], upper[MAXN][MAXN];
int deg[MAXN];
bool feasible_flow() {
// 建图
int sum = 0;
for (int u = 1; u <= n; u++) {
for (int v = 1; v <= n; v++) {
if (upper[u][v] > 0) {
add_edge(u, v, upper[u][v] - lower[u][v]);
deg[u] -= lower[u][v];
deg[v] += lower[u][v];
}
}
}
// 超级源汇
int ss = 0, tt = n + 1;
for (int i = 1; i <= n; i++) {
if (deg[i] > 0) {
add_edge(ss, i, deg[i]);
sum += deg[i];
} else if (deg[i] < 0) {
add_edge(i, tt, -deg[i]);
}
}
s = ss; t = tt;
return max_flow() == sum;
}

7.2 有源汇上下界最大流#

  1. 从汇点 tt 向源点 ss 连一条容量为 \infty 的边
  2. 按无源汇可行流求解
  3. 删除 tst \to s 的边,在残量网络上从 sstt 跑最大流

7.3 有源汇上下界最小流#

  1. 求上下界最大流
  2. 在残量网络中从 ttss 跑最大流
  3. 答案 = 上下界最大流 - 反向最大流

八、费用流的应用#

8.1 最小费用最大流问题#

例题:运输问题

nn 个仓库和 mm 个零售商,仓库 iiaia_i 个货物,零售商 jj 需要 bjb_j 个货物。从仓库 ii 运到零售商 jj 的单位费用是 cijc_{ij}。求最小运输费用。

建模

  • 源点向每个仓库连边,容量 aia_i,费用 0
  • 每个零售商向汇点连边,容量 bjb_j,费用 0
  • 仓库 ii 向零售商 jj 连边,容量 \infty,费用 cijc_{ij}

8.2 最大费用最大流#

将所有边的费用取反,跑最小费用最大流,结果再取反即可。

九、IOI级别综合题#

9.1 例题:最优乘车方案#

问题:城市有 nn 个站点和 mm 条公交线路。每条线路是一个站点序列,按顺序停靠。票价系统为:

  • 乘坐一段路程(不换乘)费用为 aa
  • 换乘费用为 bb

求从站点 ss 到站点 tt 的最小费用。

建模思路

  • 对每个站点拆点为 uinu_{in}uoutu_{out}
  • uinu_{in}uoutu_{out} 连边,容量 1,费用 0
  • 对同一线路上相邻站点 u,vu, v,连边 (uout,vin)(u_{out}, v_{in}),容量 1,费用 aa
  • 不同线路间换乘,若站点 uu 在线路 L1L_1L2L_2 上,连边表示换乘,费用 bb

9.2 例题:最小路径覆盖#

问题:给定DAG,用最少的不相交路径覆盖所有顶点。

建模

  • 拆点:每个点 vv 拆成 vLv_LvRv_R
  • 每条边 (u,v)(u,v) 转化为 uLvRu_L \to v_R
  • 源点向所有 vLv_L 连边,所有 vRv_R 向汇点连边
  • 最小路径覆盖数 = nn - 最大匹配数
int min_path_cover(int n, vector<pair<int,int>>& edges) {
// 建图
for (auto [u, v] : edges) {
add_edge(u, v + n, 1);
}
// 源汇连边
for (int i = 1; i <= n; i++) {
add_edge(s, i, 1);
add_edge(i + n, t, 1);
}
int max_match = max_flow();
return n - max_match;
}

9.3 例题:餐巾计划问题#

问题:餐厅 nn 天运营,第 ii 天需要 rir_i 条干净餐巾。有以下选择:

  • 购买新餐巾,单价 pp
  • 快洗:mm 天后可用,费用 ff
  • 慢洗:nn 天后可用,费用 ss

求最小费用方案。

建模

  • 拆点:每天拆成白天和晚上节点
  • 白天需求从源点流入,容量 rir_i,费用 0
  • 晚上脏餐巾流向汇点
  • 购买:源点到白天节点,容量 \infty,费用 pp
  • 快洗:晚上节点向 mm 天后白天节点连边,费用 ff
  • 慢洗:晚上节点向 nn 天后白天节点连边,费用 ss
  • 相邻天数脏餐巾转移边

十、复杂度总结与选择#

算法时间复杂度适用场景
Edmonds-KarpO(VE2)O(VE^2)小规模,代码简单
DinicO(V2E)O(V^2E)一般图,单位网络 O(EV)O(E\sqrt{V})
ISAPO(V2E)O(V^2E)稠密图,常数小
匈牙利O(VE)O(VE)二分图匹配
KMO(V3)O(V^3)带权匹配
MCMF (SPFA)O(nmf)O(nmf)小流量
MCMF (Dijkstra)O(fElogV)O(fE\log V)大流量

选择建议

  • CSP-S/NOI:掌握Dinic + 匈牙利 + SPFA费用流
  • IOI/ICPC:Dinic + Dijkstra费用流 + 各类建模
  • 工程应用:根据数据规模选择,注意常数优化

总结#

网络流是算法竞赛中的核心内容,需要:

  1. 理论基础:理解增广路、残量网络等概念
  2. 算法实现:熟练编写Dinic、费用流、匈牙利算法
  3. 建模能力:识别问题,转化为网络流模型
  4. 优化技巧:根据数据特点选择合适算法

掌握网络流不仅能解决图论问题,更能培养建模思维和优化意识,是从CSP-S走向IOI的必经之路。

推荐练习

  • 洛谷 P3376 最大流模板
  • 洛谷 P3381 最小费用最大流模板
  • POJ 1273, 1274, 2195
  • Codeforces 网络流专题

祝各位在算法竞赛的道路上不断进步!

网络流与二分图匹配:从CSP-S到IOI
https://myblog-590.pages.dev/posts/algorithm-network-flow/
作者
谢星宇
发布于
2026-01-21
许可协议
CC BY-NC-SA 4.0