把排水沟建成有向容量网络,用 Dinic 算法在残量网络上 BFS 分层、DFS 沿层次图增广,反向边提供反悔机制,求水潭到小溪的最大流。
OJ: roj
题目 ID: 2065
难度:普及+/提高-
标签:网络流最大流Dinic
创建: 2026-10-01 05:54
更新: 2026-10-07 13:50
形式化题目
给定一张有向图:
正解
思路
从朴素贪心到残量网络。 最直接的想法是:找一条从 1 到 1→2→4 的 20 全推给上面那条路,2→4 就满了;好在正确答案中 1→2 的 40 可以改走 2→3→4。我们需要一种"反悔"机制。
网络流的经典做法是残量网络 + 反向边:每沿边 cap[u][v] 减 cap[v][u] 加
Dinic 的两层加速。 朴素的 Ford–Fulkerson 每次只找一条增广路,最坏情形会被低效路径拖慢。Dinic 加了两个标准优化:
- 分层图:每轮开始时 BFS 一遍残量网络,算出每个点到源的层次(最短边数)。之后 DFS 只允许从层次
的点走到层次 的点,保证所有增广都沿"最短路"进行。 - 当前弧优化:每个点记录"这一轮已经试到第几条邻接边",被证明推不动的边不再重试。
每轮分层后,可以在同一张层次图上连续推送多条路径,直到沿层次图走不到汇点。汇点的层次每轮严格增加,所以最多
本题的输入形态。 平行边直接累加到 cap[s][e];自环(
下面的 Graphviz 图展示样例两轮分层增广后的结果(节点 label 为"交点:最终层次",边 label 为"容量/剩余",虚线为反向边):
digraph ditch {
rankdir=LR;
node [shape=circle];
s [label="1:0"];
a [label="2:1"];
b [label="3:2"];
t [label="4:3"];
s -> a [label="40/10", color=blue];
s -> t [label="20/0", color=blue];
a -> t [label="20/0", color=blue];
a -> b [label="30/20", color=blue];
b -> t [label="10/0", color=blue];
t -> a [label="50", style=dashed];
a -> s [label="50", style=dashed];
}
图后观察重点:第 1 轮 BFS 分层得到层次 1→2→4 的 20 和 1→4 的 20(3→4 因层次不递增暂时走不到);第 2 轮在残量网络上 4 的层次升到 3,沿 1→2→3→4 再推 10,3→4 的 10 是唯一瓶颈。合计
代码
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-10-01 06:00
# update_at: 2026-10-01 06:00
import sys
from collections import deque
INF = 1 << 60
level: list[int] = [] # 层次图:每个点在残量网络上的层次,-1 = 未访问
it: list[int] = [] # 当前弧:每个点下一次要尝试的邻接点下标
def bfs(cap: list[list[int]], s: int, t: int) -> bool:
"""在残量网络上从 s 分层,返回汇点 t 是否仍可达(还有增广路)。"""
global level
level = [-1] * len(cap)
level[s] = 0
queue = deque([s])
while queue:
u = queue.popleft()
for v, c in enumerate(cap[u]):
if c and level[v] < 0: # 该排水沟还有残量且对端没分过层
level[v] = level[u] + 1
queue.append(v)
return level[t] >= 0
def dfs(cap: list[list[int]], u: int, t: int, f: int) -> int:
"""沿层次图从 u 向 t 推一条不超过 f 的流,返回实际推送量。"""
if u == t:
return f
while it[u] < len(cap):
v = it[u]
advance = cap[u][v] > 0 and level[v] == level[u] + 1 # 只走层次+1 的残量边
if advance:
pushed = dfs(cap, v, t, min(f, cap[u][v]))
if pushed:
cap[u][v] -= pushed # 正向边扣残量,反向边留出可回退的容量
cap[v][u] += pushed
return pushed
it[u] += 1 # 这条边再也推不动,当前弧后移
return 0
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
# 邻接矩阵存容量:平行边直接累加;起点=终点的自环不贡献流量
cap = [[0] * m for _ in range(m)]
for _ in range(n):
s, e, c = next(data), next(data), next(data)
if s != e:
cap[s - 1][e - 1] += c
flow = 0
while bfs(cap, 0, m - 1): # 水潭 0 → 小溪 m-1
global it
it = [0] * m
while pushed := dfs(cap, 0, m - 1, INF):
flow += pushed
print(flow)
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-06 10:44
* update_at: 2026-10-06 10:44
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int MAXM = 205;
const ll INF = (1LL << 60);
ll n, m;
ll cap[MAXM][MAXM]; // 残量网络邻接矩阵:cap[u][v] 为 u 到 v 剩余的容量
int level[MAXM]; // 层次图:每个点在残量网络上的最短边数层次,-1 表示未访问
int cur[MAXM]; // 当前弧:每个点下一轮要从哪个邻接点开始尝试
// 在残量网络上从源点 s 分层,返回汇点 t 是否仍可达(即还有增广路)。
bool bfs(int s, int t) {
for (int i = 1; i <= m; i++) {
level[i] = -1;
}
level[s] = 0;
queue<int> q;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int v = 1; v <= m; v++) {
// 该排水沟还有残量且对端尚未分层
if (cap[u][v] > 0 && level[v] < 0) {
level[v] = level[u] + 1;
q.push(v);
}
}
}
return level[t] >= 0;
}
// 沿层次图从 u 向汇点 t 推送不超过 f 的流,返回实际推送量。
ll dfs(int u, int t, ll f) {
if (u == t) {
return f;
}
for (; cur[u] <= m; cur[u]++) {
int v = cur[u];
// 只走层次恰好 +1 的残量边
if (cap[u][v] > 0 && level[v] == level[u] + 1) {
ll pushed = dfs(v, t, min(f, cap[u][v]));
if (pushed > 0) {
cap[u][v] -= pushed; // 正向边扣残量
cap[v][u] += pushed; // 反向边留出可回退的容量
return pushed;
}
}
}
return 0;
}
void solve() {
ll flow = 0;
while (bfs(1, m)) { // 水潭 1 为源,小溪 m 为汇
for (int i = 1; i <= m; i++) {
cur[i] = 1;
}
ll pushed = dfs(1, m, INF);
while (pushed > 0) {
flow += pushed;
pushed = dfs(1, m, INF);
}
}
cout << flow << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m;
for (ll i = 1; i <= n; i++) {
int s, e;
ll c;
cin >> s >> e >> c;
// 平行边直接累加;自环不贡献流量,跳过
if (s != e) {
cap[s][e] += c;
}
}
solve();
return 0;
}复杂度
- 时间复杂度:Dinic 为
。本题 ,实测全部 12 组官方数据在 0.02s 内通过。 - 空间复杂度:邻接矩阵
,加上分层与当前弧数组 ,约 160KB,远低于 128MB 限制。
总结
- 本题是最大流模板题:排水沟 = 有向容量边,水潭 = 源,小溪 = 汇。
- 贪心推流会错,反向边提供"反悔"机制,增广路定理保证终止在最大流。
- Dinic 用 BFS 分层 + DFS 当前弧做多层增广,复杂度
,对 绰绰有余。 - 实现细节:平行边累加、自环跳过、容量 0 的边自然被过滤。 EOF