学习笔记:网络流
本文主要由本人写作,但经过 AI 润色使其结构更加清晰、行文更加流畅,文风可能与 AI 类似,请谅解。
引入
想象一个城市的输水系统。自来水厂(水源)通过管道网络向千家万户供水,用户使用后的废水汇入处理厂,最终排入河流。
若将水源、用户、处理厂视为结点,管道视为有向边,管道的水流上限视为容量,这就构成了一个网络。研究水流如何在该网络中分配与传输,就是网络流问题。
基础概念
一个网络是一个有向图 $ G = (V, E)$ ,其中:
- 有两个特殊结点:源点 $ s$ (如水源)和汇点 $ t$ (如废水处理厂)。
- 每条有向边 $ (u, v)$ 有一个容量 $ c(u, v) \geq 0$ ,表示该边能承载的最大流量。
该网络上的一个流 $ f$ 是为每条边分配的一个实数 $ f(u, v)$ ,满足:
- 容量限制:$ 0 \leq f(u, v) \leq c(u, v)$ 。
- 流量守恒:除源点 $ s$ 和汇点 $ t$ 外,每个结点 $ u$ 满足:\[\sum_{v \in V} f(u, v) = \sum_{v \in V} f(v, u) \]即流入等于流出。
定义结点 $ u$ 的净流出量为流出总量减去流入总量。根据守恒性,对于中间结点净流出为 0。源点 $ s$ 的净流出称为该流的流量,记作 $ |f|$ 。
常见问题
最大流问题
给定网络,寻找一个流 $ f$ ,使其流量 $ |f| $ 最大。
Dinic 算法
Dinic 算法是一种高效的最大流算法,时间复杂度为 $ O(V^2 E)$ (最坏情况),实际运行通常更快。
流程图:
算法流程如下:
-
构建残量网络:
- 对于原图每条边 $ (u, v)$ ,添加一条反向边 $ (v, u)$ ,初始容量为 0。
- 反向边的作用是提供“反悔”机制:当后续发现更优的流量分配时,可以通过反向边退回部分流量,从而重新调整。
-
BFS 构建分层图:
- 从源点 $ s$ 开始 BFS,标记每个结点的深度(距离 $ s$ 的最短边数)。
- 只有从深度 $ d$ 的结点指向深度 $ d+1$ 的结点的边才被保留,形成分层图。
-
DFS 寻找增广路:
- 在分层图上进行 DFS,寻找一条从 $ s$ 到 $ t$ 的路径。
- 沿路径增加流量,增加量为路径上最小的残余容量。
- 更新残量网络:正向边容量减少,反向边容量增加。
-
重复执行:
- 当一次 DFS 无法再找到增广路时,返回步骤 2,重新 BFS 构建新的分层图。
- 直到 BFS 无法到达 $ t$ 为止,算法结束。此时总流量即为最大流。
常用优化:
- 当前弧优化:在 DFS 中,每条边一旦被访问过,在当前分层图中就不再重复访问。
- 多路增广:一次 DFS 可找到并增广多条路径。
- 炸点优化:若从某点出发无法推送任何流量到 $ t$ ,则将其深度标记为无效,避免后续无效访问。
洛谷 P3376 【模板】网络最大流 参考代码
#include <bits/stdc++.h>
#define int long long
const int MIN = 0xc0c0c0c0c0c0c0c0, MAX = 0x3f3f3f3f3f3f3f3f;
const int N = 210, M = 5010;
struct Edge {
int to, cap, next;
} g[M * 2];
int head[N], idx, curr[N], n, m, s, t, dep[N];
void addEdge(int u, int v, int w) {
g[idx] = {v, w, head[u]};
head[u] = idx++;
}
bool bfs(int s, int t) {
memset(dep, -1, sizeof(int) * (n + 1));
dep[s] = 0;
std::queue<int> q;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i != -1; i = g[i].next) {
int v = g[i].to;
if (dep[v] == -1 && g[i].cap > 0) {
dep[v] = dep[u] + 1;
if (v == t) return true;
q.push(v);
}
}
}
return false;
}
int dfs(int u, int t, int rest) { // 当前弧优化
if (u == t) return rest;
int flow = 0;
for (int &i = curr[u]; i != -1; i = g[i].next) {
int v = g[i].to;
if (dep[v] == dep[u] + 1 && g[i].cap > 0) {
int d = dfs(v, t, std::min(rest, g[i].cap));
if (d > 0) {
g[i].cap -= d; // 更新正向边
g[i^1].cap += d; // 更新反向边
flow += d;
rest -= d;
if (rest == 0) break; // 流量已用完,及时退出
}
}
}
if (flow == 0) dep[u] = -1; // 炸点
return flow;
}
int dinic(int s, int t) {
int res = 0;
while (bfs(s, t)) {
for (int i = 1; i <= n; i++) curr[i] = head[i]; // 为本次 BFS 分层后的所有 DFS 设置当前弧
int f = 0;
while ((f = dfs(s, t, MAX)) > 0) res += f;
}
return res;
}
signed main() {
std::ios::sync_with_stdio(false); std::cin.tie(0);
std::cin >> n >> m >> s >> t;
memset(head, -1, sizeof(int) * (n + 1));
for (int i = 1; i <= m; i++) {
int u, v, w;
std::cin >> u >> v >> w;
addEdge(u, v, w);
addEdge(v, u, 0); // 构建反向边
}
std::cout << dinic(s, t) << '\n';
return 0;
}
最大流最小割定理
割:将结点集 $ V$ 划分为两部分 $ S$ 和 $ T = V \setminus S$ ,且 $ s \in S, t \in T$ 。割 $ (S, T)$ 的容量定义为所有从 $ S$ 指向 $ T$ 的边的容量之和:
最小割是容量最小的割。
最大流最小割定理:在一个网络中,最大流的流量等于最小割的容量。该定理揭示了流与割的对偶关系,证明可参考 https://oi-wiki.org/graph/flow/max-flow/#最大流最小割定理。
最小费用最大流问题
在最大流问题基础上,每条边还有一个单位流量费用 $ w(u, v)$ 。目标是找到最大流中总费用最小者。
算法思路(SPFA 费用流):
- 建图:同时建立容量为 0、费用为反向数($ -w$ )的反向边,以实现“退货退款”。
- 寻找最短路:在残量网络中,以单位费用为边权,用 SPFA 寻找从 $ s$ 到 $ t$ 的最短费用增广路。因存在负权边(反向边),通常使用 SPFA 或经过势函数处理的 Dijkstra。
- 沿路增广:
- 增广量 $ \Delta = \min {\text{路径上各边残余容量} }$ 。
- 更新流量和费用。
- 重复:直到无法找到增广路。
洛谷 P3381 【模板】最小费用最大流 参考代码
#include <bits/stdc++.h>
#define int long long
const int MIN = 0xc0c0c0c0c0c0c0c0, MAX = 0x3f3f3f3f3f3f3f3f;
const int N = 5e3+10, M = 5e4+10;
struct Edge {
int to, next, cap, cost;
} g[M * 2];
int head[N], idx, pre[N], dis[N], incf[N]; bool vis[N];
int n, m, s, t;
int maxFlow, minCost;
void addEdge(int u, int v, int cap, int cost) {
g[idx] = {v, head[u], cap, cost};
head[u] = idx++;
}
bool spfa() {
std::queue<int> q;
memset(dis, 0x3f, sizeof(int) * (n + 1));
memset(vis, 0, sizeof(int) * (n + 1));
q.push(s);
dis[s] = 0; vis[s] = true;
incf[s] = MAX;
while (!q.empty()) {
int u = q.front();
q.pop();
vis[u] = false;
for (int i = head[u]; i != -1; i = g[i].next) {
int v = g[i].to, len = g[i].cost;
if (dis[u] + len < dis[v] && g[i].cap > 0) {
dis[v] = dis[u] + len;
pre[v] = i;
incf[v] = std::min(incf[u], g[i].cap);
if (!vis[v]) {
q.push(v);
vis[v] = true;
}
}
}
}
return dis[t] != MAX;
}
void MCMF() {
maxFlow = 0, minCost = 0;
while (spfa()) {
if (incf[t] == 0) break;
int flow = incf[t];
maxFlow += flow;
minCost += flow * dis[t];
for (int u = t; u != s; u = g[pre[u] ^ 1].to) {
g[pre[u]].cap -= flow;
g[pre[u] ^ 1].cap += flow;
}
}
}
signed main() {
std::ios::sync_with_stdio(false); std::cin.tie(0);
std::cin >> n >> m >> s >> t;
memset(head, -1, sizeof(int) * (n + 1));
for (int i = 1; i <= m; i++) {
int u, v, cost, cap;
std::cin >> u >> v >> cap >> cost;
addEdge(u, v, cap, cost);
addEdge(v, u, 0, -cost);
}
MCMF();
std::cout << maxFlow << ' ' << minCost << '\n';
return 0;
}
建模实战
掌握算法模板后,真正的挑战在于将实际问题转化为网络流模型。下面通过例题体会建模思路。
洛谷 P3254 圆桌问题
题意:有 $ m$ 个单位,每个单位有 $ r_i$ 名代表;有 $ n$ 张圆桌,每张可容纳 $ c_j$ 人。要求同一单位的代表不在同一桌就餐。问是否存在可行方案,并输出方案。
建模:
- 建立超级源点 $ S$ 和超级汇点 $ T$ 。
- $ S$ 向每个单位 $ i$ 连边,容量为 $ r_i$ (该单位代表数)。
- 每个圆桌 $ j$ 向 $ T$ 连边,容量为 $ c_j$ (该桌容纳量)。
- 每个单位 $ i$ 向每张圆桌 $ j$ 连边,容量为 \(1\)(保证每桌至多一名该单位代表)。
若最大流等于总人数,则有解。输出方案时,检查单位 $ i$ 到圆桌 $ j$ 的边流量是否为 0(即被使用)。
参考代码
#include <bits/stdc++.h>
#define int long long
const int _INF = 0xc0c0c0c0c0c0c0c0, INF = 0x3f3f3f3f3f3f3f3f;
const int N = 150 + 270 + 10, M = 2 * (150 * 270 + 10);
struct Graph {
struct Edge {
int to, cap, next;
} g[M];
int head[N], idx, curr[N], n, m, s, t, dep[N];
Graph() {
memset(head, -1, sizeof(head));
}
void addEdge(int u, int v, int w) {
g[idx] = {v, w, head[u]};
head[u] = idx++;
}
bool bfs(int s, int t) {
memset(dep, -1, sizeof(int) * (n + 1));
dep[s] = 0;
std::queue<int> q;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i != -1; i = g[i].next) {
int v = g[i].to;
if (dep[v] == -1 && g[i].cap > 0) {
dep[v] = dep[u] + 1;
if (v == t) return true;
q.push(v);
}
}
}
return false;
}
int dfs(int u, int t, int rest) {
if (u == t) return rest;
int flow = 0;
for (int &i = curr[u]; i != -1; i = g[i].next) {
int v = g[i].to;
if (dep[v] == dep[u] + 1 && g[i].cap > 0) {
int d = dfs(v, t, std::min(rest, g[i].cap));
if (d > 0) {
g[i].cap -= d;
g[i^1].cap += d;
flow += d;
rest -= d;
if (rest == 0) break;
}
}
}
if (flow == 0) dep[u] = -1;
return flow;
}
int dinic(int s, int t) {
int res = 0;
while (bfs(s, t)) {
for (int i = 0; i <= n; i++) curr[i] = head[i];
int f = 0;
while ((f = dfs(s, t, INF)) > 0) res += f;
}
return res;
}
} g;
signed main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
std::ios::sync_with_stdio(false); std::cin.tie(0);
// 1 为超级源点,m + n + 2 为超级汇点
int m, n;
std::cin >> m >> n;
int sumR = 0;
for (int i = 2; i <= m + 1; i++) {
int r;
std::cin >> r;
sumR += r;
g.addEdge(1, i, r);
g.addEdge(i, 1, 0);
}
for (int i = m + 2; i <= m + n + 1; i++) {
int c;
std::cin >> c;
g.addEdge(i, m + n + 2, c);
g.addEdge(m + n + 2, i, 0);
}
for (int i = 2; i <= m + 1; i++) {
for (int j = m + 2; j <= m + n + 1; j++) {
g.addEdge(i, j, 1);
g.addEdge(j, i, 0);
}
}
g.n = n + m + 2; g.m = n + m + n * m;
int res = g.dinic(1, m + n + 2);
if (res == sumR) {
std::cout << "1\n";
for (int i = 2; i <= m + 1; i++) {
std::vector<int> ans;
for (int j = g.head[i]; j != -1; j = g.g[j].next) {
if (g.g[j].to >= m + 2 && g.g[j].to <= m + n + 1 && g.g[j].cap == 0) {
ans.push_back(g.g[j].to - m - 1);
}
}
for (auto const &num : ans) {
std::cout << num << ' ';
}
std::cout << '\n';
}
} else {
std::cout << "0\n";
}
return 0;
}
洛谷 P2762 太空飞行计划问题
题意:有 $ m$ 个实验,每个实验有收益 $ p_i$ ;有 $ n$ 种仪器,每个仪器有花费 $ c_j$ 。进行实验需要购买所需仪器。求最大净收益(总收益−仪器花费)及方案。
建模(最大权闭合子图):
- 建立超级源点 $ S$ 和超级汇点 $ T$ 。
- $ S$ 向每个实验 $ i$ 连边,容量为 $ p_i$ 。
- 每个仪器 $ j$ 向 $ T$ 连边,容量为 $ c_j$ 。
- 每个实验向其所需仪器连边,容量为 $ +\infty$ (确保依赖关系)。
原理:该模型的最小割对应最优选择。
- 割边 $ S \to$ 实验 表示放弃该实验(损失收益)。
- 割边 仪器 $ \to T$ 表示购买该仪器(支付花费)。
- 无限边迫使:若选择实验,则必须购买其所需仪器(否则割容量无穷大)。
计算最大流(最小割),净收益 = 总收益 − 最大流。方案为最后一次 BFS 中可达的结点(实验和仪器)。
参考代码
// 超级源点 1,实验 2 ~ m+1,仪器 m+2 ~ m+n+1,超级汇点 m+n+2
#include <bits/stdc++.h>
#define int long long
const int _INF = 0xc0c0c0c0c0c0c0c0, INF = 0x3f3f3f3f3f3f3f3f;
const int N = 110, M = 2 * (50 * 50 + 100) + 10;
struct Graph {
int head[N], curr[N], n, idx, dep[N];
Graph() {
memset(head, -1, sizeof(head));
}
struct Edge {
int to, cap, next;
} g[M];
void addEdge(int u, int v, int w) {
g[idx] = {v, w, head[u]};
head[u] = idx++;
g[idx] = {u, 0, head[v]};
head[v] = idx++;
}
bool bfs(int s, int t) {
memset(dep, -1, sizeof(int) * (n + 1));
dep[s] = 0;
std::queue<int> q;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i != -1; i = g[i].next) {
int v = g[i].to;
if (dep[v] == -1 && g[i].cap > 0) {
dep[v] = dep[u] + 1;
if (v == t) return true;
q.push(v);
}
}
}
return false;
}
int dfs(int u, int t, int rest) {
if (u == t) return rest;
int flow = 0;
for (int &i = curr[u]; i != -1; i = g[i].next) {
int v = g[i].to;
if (dep[v] == dep[u] + 1 && g[i].cap > 0) {
int d = dfs(v, t, std::min(rest, g[i].cap));
if (d > 0) {
g[i].cap -= d;
g[i^1].cap += d;
flow += d;
rest -= d;
if (rest == 0) break;
}
}
}
if (flow == 0) dep[u] = -1;
return flow;
}
int dinic(int s, int t) {
int res = 0;
while (bfs(s, t)) {
memcpy(curr, head, sizeof(int) * (n + 1));
int f = 0;
while ((f = dfs(s, t, INF)) > 0) res += f;
}
return res;
}
} g;
signed main() {
// freopen(".in", "r", stdin);
// freopen(".out", "w", stdout);
std::ios::sync_with_stdio(false); std::cin.tie(0);
int m, n;
std::cin >> m >> n;
g.n = m + n + 2;
int tot = 0;
for (int i = 1; i <= m; i++) {
int val;
std::cin >> val;
tot += val;
g.addEdge(1, i+1, val);
char tools[10010];
memset(tools, 0, sizeof(tools));
std::cin.getline(tools, 10000);
int ulen = 0, tool;
while (sscanf(tools + ulen, "%d", &tool) == 1) {
g.addEdge(i+1, m+1+tool, INF);
if (tool == 0) {
ulen++;
} else {
while (tool) {
tool /= 10;
ulen++;
}
}
ulen++;
}
}
for (int i = 1; i <= n; i++) {
int cost;
std::cin >> cost;
g.addEdge(m+i+1, m+n+2, cost);
}
int res = g.dinic(1, m+n+2);
for (int i = 1; i <= m; i++) {
if (g.dep[1+i] != -1) {
std::cout << i << ' ';
}
}
std::cout << '\n';
for (int i = 1; i <= n; i++) {
if (g.dep[m+1+i] != -1) {
std::cout << i << ' ';
}
}
std::cout << '\n';
std::cout << tot - res << '\n';
return 0;
}
建模常见特征
- 资源分配/匹配:如单位与圆桌、任务与机器。
- 依赖/选择:如实验依赖仪器,用无限容量边表示强制关联。
- 容量限制:边的容量常表示数量上限、花费等。
- 最大化/最小化:通过最大流、最小割或费用流实现。
总结
网络流是图论中强大的建模工具,其核心在于将实际问题抽象为流量分配与路径寻找。熟练掌握最大流、最小割及费用流算法后,重点应培养识别问题特征并巧妙建图的能力。

浙公网安备 33010602011771号