把有向沟渠网络看作流网络(容量=速率,源=1,汇=M),用 Dinic 的 BFS 分层 + DFS 阻塞流 + 当前弧求最大流。
OJ: roj
题目 ID: 3224
难度:提高+/省选-
标签:图论网络流最大流Dinic
创建: 2026-10-02 03:11
更新: 2026-10-10 11:30
形式化题目
给定一张有向图
求一个满足下面两个约束的流
也就是求从
以样例为例,5 条沟渠构成的流网络如下(箭头上的数字是容量)。
graph LR
v1((1 池塘))
v2((2))
v3((3))
v4((4 河))
v1 -->|40| v2
v1 -->|20| v4
v2 -->|20| v4
v2 -->|30| v3
v3 -->|10| v4
读者可以观察到三件事:
正解
思路
第一步:为什么"找一条最宽的路"不对
最朴素的想法是:找一条从
所以答案不是一条路,而是一组互相共享容量、又都不超限的"流量分配"。这正是流的定义:
每条弧上有流量
第二步:贪心推路径为什么必须能"撤销"
第二个想法是:反复找一条"还有剩余容量的路",沿路把瓶颈流量推满并累加,直到找不到为止。
这个方向是对的,但只推正向会卡住。看一个最小反例:容量为
做法是维护残量网络
第三步:用增广路定理给出终止条件
设
反过来,若
增广路定理:
是最大流 中不存在 的路。
这条定理就是"不断增广、直到无路可走"这一族算法(Ford–Fulkerson 思路)的正确性依据。
第四步:Dinic —— 把"每轮一条路"变成"每轮一层阻塞流"
朴素的每次只增广一条路,轮数可能和答案大小成正比(这里
- BFS 分层:在
上只沿 cap > 0的弧走,记level[v]为到 的最少弧数; 若 level[t] < 0,由增广路定理当前流即最大流,返回。 - DFS 推阻塞流:只保留满足
level[v] == level[u] + 1的弧, 在这张分层图上反复找路并推满,直到图中再也没有这样的路 (此时推到的流量叫阻塞流)。 - 回到第 1 步。
分层带来两个好处:
- 路径不会绕圈:每条弧层号严格
+1,路长被限制在以内; - 轮数有界:一轮结束意味着分层图上所有短的路都被堵死,
新的最短增广路必须变长,因此
level[t]每轮严格增大,外层最多轮,与 的大小无关。
第五步:当前弧与回溯优化
- DFS 中如果某条出弧被判定走不通(容量为
或层号不接),它在本轮永远不会再走通, 用一个指针 it[u]记住扫到哪,下次从它继续,不再从头扫出边。 - 递归写法在 Python 中既有栈深风险又慢,代码里用显式栈
path(元素是(点, 出边号))模拟:- 走到汇点后沿
path扣正弧、加反弧,然后退回到第一条被榨干的弧继续, 而不是退回源点重来; - 走到死路时把该点的
level置-1(本轮永久废弃)并弹栈回溯。
- 走到汇点后沿
下面这张表是用代码里的算法在样例上逐轮打印的真实过程,用来把上面的抽象步骤落到具体数字。
| 轮次 | BFS 得到的 level |
本轮增广的路径 | 瓶颈 |
累计流量 |
|---|---|---|---|---|
| 1 | 1:0, 2:1, 3:2, 4:1 |
20 | 20 | |
| 2 | 1:0, 2:1, 3:2, 4:2 |
20 | 40 | |
| 3 | 1:0, 2:1, 3:2, 4:3 |
10 | 50 |
表格每行是一轮:第一列是轮次,第二列是该轮 BFS 得到的分层结果(只列可达点,level 就是它离源点的弧数),
第三列是该轮被推的增广路,第四、五列分别是这条路的瓶颈与累加后的总流量。观察重点:
- 第 1 轮选中的是最短的
(层差 ),而不是路长更大的 ; Dinic 每轮只在意"最短"这一件事。 - 第 2 轮汇点的层号从
变成 ,直观展示了"最短增广路长度单调递增", 这正是外层最多 轮的来源。 - 第 3 轮之后
的两条出弧全部满流、汇点不可达,算法返回 ; 此时进入 的三条弧容量和 被恰好用满,答案不可能更大。
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int NO_EDGE = -1;
const int UNREACHED = -1;
int head[205];
vector<int> to, nxt;
vector<ll> cap;
void add_edge(int u, int v, ll c) {
to.push_back(v); cap.push_back(c); nxt.push_back(head[u]); head[u] = (int)to.size() - 1;
to.push_back(u); cap.push_back(0); nxt.push_back(head[v]); head[v] = (int)to.size() - 1;
}
int level[205];
// BFS 分层
bool build_levels(int s, int t, int m) {
for (int i = 1; i <= m; i++) level[i] = UNREACHED;
level[s] = 0;
deque<int> q;
q.push_back(s);
while (!q.empty()) {
int u = q.front(); q.pop_front();
int e = head[u];
while (e != NO_EDGE) {
int v = to[e];
if (cap[e] > 0 && level[v] == UNREACHED) {
level[v] = level[u] + 1;
q.push_back(v);
}
e = nxt[e];
}
}
return level[t] != UNREACHED;
}
// 在当前分层图上推阻塞流
ll blocking_flow(int s, int t) {
vector<int> it(head, head + 205);
vector<pair<int, int> > path; // (点, 走出的弧号)
ll flow = 0;
int u = s;
while (true) {
if (u == t) {
ll bottle = 1LL << 60;
for (size_t i = 0; i < path.size(); i++)
bottle = min(bottle, cap[path[i].second]);
for (size_t i = 0; i < path.size(); i++) {
cap[path[i].second] -= bottle;
cap[path[i].second ^ 1] += bottle;
}
flow += bottle;
size_t first = 0;
while (first < path.size() && cap[path[first].second] != 0) first++;
path.resize(first);
u = path.empty() ? s : to[path.back().second];
} else {
int e = it[u];
int advance = NO_EDGE;
while (e != NO_EDGE) {
int v = to[e];
if (cap[e] > 0 && level[v] == level[u] + 1) { advance = e; break; }
e = nxt[e];
}
it[u] = e;
if (advance != NO_EDGE) {
path.push_back(make_pair(u, advance));
u = to[advance];
} else if (path.empty()) {
break;
} else {
level[u] = UNREACHED;
u = path.back().first;
path.pop_back();
}
}
}
return flow;
}
ll max_flow(int s, int t, int m) {
ll flow = 0;
while (build_levels(s, t, m)) {
flow += blocking_flow(s, t);
}
return flow;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
int s = 1, t = m;
for (int i = 1; i <= m; i++) head[i] = NO_EDGE;
for (int i = 0; i < n; i++) {
int u, v;
ll c;
cin >> u >> v >> c;
add_edge(u, v, c);
}
cout << max_flow(s, t, m) << "\n";
return 0;
}#!/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-02 03:11
# update_at: 2026-10-02 03:16
import sys
from collections import deque
NO_EDGE = -1 # 链式前向星:没有下一条弧
UNREACHED = -1 # BFS 分层:本轮还没访问到 / 从 s 到不了
def add_edge(head: list[int], to: list[int], nxt: list[int], cap: list[int],
u: int, v: int, c: int) -> None:
"""加一条 u->v 容量 c 的弧,紧跟一条容量 0 的反向弧:两条弧编号互为 e ^ 1。
反向弧的容量表示"这条正向弧上已推了多少水",走它就等于把水退回去。
"""
to.append(v)
cap.append(c)
nxt.append(head[u])
head[u] = len(to) - 1
to.append(u)
cap.append(0)
nxt.append(head[v])
head[v] = len(to) - 1
def build_levels(head: list[int], to: list[int], nxt: list[int], cap: list[int],
s: int) -> list[int]:
"""BFS 分层:只沿还有剩余容量的弧走,返回每点离 s 的弧数,够不到记 UNREACHED。"""
level = [UNREACHED] * len(head)
level[s] = 0
queue = deque([s])
while queue:
u = queue.popleft()
e = head[u]
while e != NO_EDGE:
v = to[e]
if cap[e] > 0 and level[v] == UNREACHED:
level[v] = level[u] + 1
queue.append(v)
e = nxt[e]
return level
def blocking_flow(head: list[int], to: list[int], nxt: list[int], cap: list[int],
level: list[int], s: int, t: int) -> int:
"""在当前分层图上推阻塞流:只走层号严格 +1 的弧,直到再也走不到 t,返回流量。
用显式栈代替递归:栈里存 (点, 从它走出去的弧号)。层号接不上的弧本轮永远接不上,
由当前弧 it[u] 跳过;某点的出弧全废就把它的层号抹掉,回退时不再进来。
"""
it = head[:] # 当前弧:每条弧走废后本轮不再重扫
path: list[tuple[int, int]] = []
flow = 0
u = s
while True:
if u == t:
bottle = min(cap[e] for _, e in path)
for _, e in path:
cap[e] -= bottle
cap[e ^ 1] += bottle # 反向弧加容量 = 允许把这段水退回去
flow += bottle
# 只退回到第一条被榨干的弧,从那里接着找下一条增广路
first = next(i for i, (_, e) in enumerate(path) if cap[e] == 0)
path = path[:first]
u = to[path[-1][1]] if path else s
else:
e, advance = it[u], NO_EDGE
while e != NO_EDGE:
v = to[e]
if cap[e] > 0 and level[v] == level[u] + 1:
advance = e # 分层图上唯一可走的出弧
break
e = nxt[e]
it[u] = e
if advance != NO_EDGE:
path.append((u, advance))
u = to[advance]
elif not path: # 源点的弧全废,本轮阻塞流已挖空
break
else: # 死路:抹掉层号并退回上一个点
level[u] = UNREACHED
u = path.pop()[0]
return flow
def max_flow(head: list[int], to: list[int], nxt: list[int], cap: list[int],
s: int, t: int) -> int:
"""Dinic:反复"分层 + 推阻塞流",直到残量网络里 t 不可达。"""
flow = 0
while True:
level = build_levels(head, to, nxt, cap, s)
if level[t] == UNREACHED: # 无增广路,当前流已是最大流
return flow
flow += blocking_flow(head, to, nxt, cap, level, s, t)
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
n, m = next(data), next(data)
s, t = 1, m # 交叉点 1 是池塘(源),交叉点 m 是河(汇)
head = [NO_EDGE] * (m + 1)
to: list[int] = []
nxt: list[int] = []
cap: list[int] = []
for _ in range(n):
u, v, c = next(data), next(data), next(data)
add_edge(head, to, nxt, cap, u, v, c) # 水只能沿 Si -> Ei 流
print(max_flow(head, to, nxt, cap, s, t))
if __name__ == "__main__":
solve()复杂度
记
- BFS 分层一次是
,外层最多 轮(最短增广路长度每轮严格增大,上限 ); - 每轮找阻塞流:当前弧
it[u]保证每条弧最多被推进或作废一次, 而每次成功增广都榨干至少一条弧(所以一轮里增广次数不超过),每次增广回溯不超过 条弧, 于是单轮是 。
因此总复杂度为
空间上只用链式前向星的 level、it 数组,即
总结
- 沟渠的方向就是弧的方向、速率就是容量,问题直接等价于单源单汇最大流, 不需要任何额外建模(不同于二分图匹配题要加源汇点)。
- 关键转弯有两处:一是意识到答案不是一条路,必须允许流量在多条路之间共享; 二是允许"撤销",用反向弧把撤销变成又一次普通的增广。
- 增广路定理给出了简洁的终止条件;Dinic 用 BFS 分层把每轮从"一条路"升级为"一张分层图上的阻塞流", 使轮数只与点数有关而与容量无关;当前弧 + 显式栈回溯是让它跑得动 Python 的常数优化。
- 边界情况(
、容量全 、源无出弧、重边、自环、环)都被同一套流程自然覆盖,代码里没有任何特判。