网络流与二分图匹配:从CSP-S到IOI
网络流是图论中最重要的算法之一,广泛应用于资源分配、任务调度、匹配问题等场景。本文将系统介绍从入门到进阶的网络流算法体系。
一、网络流基本概念
1.1 流网络定义
流网络是一个有向图 ,其中:
- 每条边 有一个非负容量
- 存在源点 和汇点 ,其中 没有入边, 没有出边
- 对于不存在的边 ,规定
1.2 流的性质
流 是定义在边集上的函数,满足:
容量限制:
流量守恒:
流的值定义为:
1.3 残量网络与增广路
残量网络 中,每条边的残量容量为:
增广路是残量网络中从 到 的一条路径,沿途所有边的残量容量都大于0。
增广路定理:流 是最大流当且仅当残量网络 中不存在增广路。
二、最大流算法
2.1 Ford-Fulkerson 方法
Ford-Fulkerson 是一种框架性方法,核心思想是不断寻找增广路并增广。
算法流程:
- 初始化流 为 0
- 当存在增广路 时:
- 计算 的残量容量
- 沿 增广流量
- 返回流
时间复杂度:,其中 是最大流的值。
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;}时间复杂度:
2.3 Dinic 算法
Dinic 算法通过构造分层图和多路增广优化效率。
关键优化:
- 分层图:使用BFS构造按最短路长度分层的图
- 当前弧优化:记录每个节点当前搜索到的边,避免重复搜索
- 多路增广:一次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;}时间复杂度:,单位容量网络中为
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;}时间复杂度:
三、最小费用最大流
最小费用最大流问题在最大流的基础上,每条边增加单位流量费用 ,目标是在流量最大的前提下使总费用最小。
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};}时间复杂度:,其中 是最大流
3.2 势函数优化的Dijkstra
使用势函数 将边权变为非负,从而使用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;}时间复杂度:
四、二分图匹配
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;}时间复杂度:
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;}时间复杂度:
五、最小割与最大流定理
5.1 最大流最小割定理
定理:网络中最大流的值等于最小割的容量。
割 是将顶点集 划分为两个集合,其中 。
割的容量:
证明思路:
- 任何流的值 任何割的容量
- 找到一个流和一个割使得它们相等
5.2 最小割应用
求最小割只需在最大流算法结束后,在残量网络中从源点BFS能到达的点属于 ,其余属于 。
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 拆点技巧
点容量限制:将点 拆成 和 ,连边 。
6.2 多源多汇
建立超级源点 和超级汇点 :
- 从 向所有源点连容量为 的边
- 从所有汇点向 连容量为 的边
6.3 最大权闭合子图
问题:给定带权有向图,求权值和最大的闭合子图。
建模:
- 源点向所有正权点连边,容量为权值
- 所有负权点向汇点连边,容量为权值的绝对值
- 原图边容量为
- 答案 = 正权和 - 最小割
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 无源汇上下界可行流
每条边 有流量下界 和上界 。
转化方法:
- 对每条边 ,强制流量至少为
- 建立新图,边容量为
- 对每个点维护初始盈亏度:
- 建立超级源汇, 时从源点连边, 时连向汇点
- 求最大流,若流量等于所有从源点出发的边容量和,则存在可行流
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 有源汇上下界最大流
- 从汇点 向源点 连一条容量为 的边
- 按无源汇可行流求解
- 删除 的边,在残量网络上从 到 跑最大流
7.3 有源汇上下界最小流
- 求上下界最大流
- 在残量网络中从 到 跑最大流
- 答案 = 上下界最大流 - 反向最大流
八、费用流的应用
8.1 最小费用最大流问题
例题:运输问题
有 个仓库和 个零售商,仓库 有 个货物,零售商 需要 个货物。从仓库 运到零售商 的单位费用是 。求最小运输费用。
建模:
- 源点向每个仓库连边,容量 ,费用 0
- 每个零售商向汇点连边,容量 ,费用 0
- 仓库 向零售商 连边,容量 ,费用
8.2 最大费用最大流
将所有边的费用取反,跑最小费用最大流,结果再取反即可。
九、IOI级别综合题
9.1 例题:最优乘车方案
问题:城市有 个站点和 条公交线路。每条线路是一个站点序列,按顺序停靠。票价系统为:
- 乘坐一段路程(不换乘)费用为
- 换乘费用为
求从站点 到站点 的最小费用。
建模思路:
- 对每个站点拆点为 和
- 到 连边,容量 1,费用 0
- 对同一线路上相邻站点 ,连边 ,容量 1,费用
- 不同线路间换乘,若站点 在线路 和 上,连边表示换乘,费用
9.2 例题:最小路径覆盖
问题:给定DAG,用最少的不相交路径覆盖所有顶点。
建模:
- 拆点:每个点 拆成 和
- 每条边 转化为
- 源点向所有 连边,所有 向汇点连边
- 最小路径覆盖数 = - 最大匹配数
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 例题:餐巾计划问题
问题:餐厅 天运营,第 天需要 条干净餐巾。有以下选择:
- 购买新餐巾,单价
- 快洗: 天后可用,费用
- 慢洗: 天后可用,费用
求最小费用方案。
建模:
- 拆点:每天拆成白天和晚上节点
- 白天需求从源点流入,容量 ,费用 0
- 晚上脏餐巾流向汇点
- 购买:源点到白天节点,容量 ,费用
- 快洗:晚上节点向 天后白天节点连边,费用
- 慢洗:晚上节点向 天后白天节点连边,费用
- 相邻天数脏餐巾转移边
十、复杂度总结与选择
| 算法 | 时间复杂度 | 适用场景 |
|---|---|---|
| Edmonds-Karp | 小规模,代码简单 | |
| Dinic | 一般图,单位网络 | |
| ISAP | 稠密图,常数小 | |
| 匈牙利 | 二分图匹配 | |
| KM | 带权匹配 | |
| MCMF (SPFA) | 小流量 | |
| MCMF (Dijkstra) | 大流量 |
选择建议:
- CSP-S/NOI:掌握Dinic + 匈牙利 + SPFA费用流
- IOI/ICPC:Dinic + Dijkstra费用流 + 各类建模
- 工程应用:根据数据规模选择,注意常数优化
总结
网络流是算法竞赛中的核心内容,需要:
- 理论基础:理解增广路、残量网络等概念
- 算法实现:熟练编写Dinic、费用流、匈牙利算法
- 建模能力:识别问题,转化为网络流模型
- 优化技巧:根据数据特点选择合适算法
掌握网络流不仅能解决图论问题,更能培养建模思维和优化意识,是从CSP-S走向IOI的必经之路。
推荐练习:
- 洛谷 P3376 最大流模板
- 洛谷 P3381 最小费用最大流模板
- POJ 1273, 1274, 2195
- Codeforces 网络流专题
祝各位在算法竞赛的道路上不断进步!