网络流学习笔记
网络流学习笔记
一、定义
网络(network)是指一个特殊的有向图
中的每条边 都有一个被称为容量 (capacity) 的权值,记作 )。当 时,可以假定 。
这句话的意思是对于网络流中的反向边,流量应为
。
中有两个特殊的点:源点(source) 和汇点(sink) ( )。
对于网络 ,流(flow)是一个从边集 E 到整数集或实数集的函数,其满足以下性质。
容量限制:对于每条边,流经该边的流量不得超过该边的容量,即
流守恒性:除源汇点外,任意结点
流出多少,也应该流入多少。
对于网络
是源点, 是 流出的流量,就等于 流入的流量 。
斜对称性:对于每条边,
这是后文反向边与反悔策略需要用到的。
对于网络
割通俗来讲就是将一张网络断掉一些边使得其不再连通的的边集。割的容量就是这个边集的流量和。
二、最大流问题
1. 最大流算法
(1) 增广路
增广路指的是原图中一条
(2) FF 算法
在原图中每一次找到一条增广路进行增广,直到原图中不存在增广路。
(3) 反向边与撤销影响
是对上面的算法正确性的一个证明。显然每一次增广路的选择是偶然的。
考虑这样一张网络:
显然最优方案是
由于网络流斜对称性质,此时的流量
考虑这样做的实际意义:
于是我们证明了 FF 算法的正确性。
(4) EK 算法
每次通过 bfs 找到一条增广路,进行增广。是对 FF 算法的一个具体实现。
(5) Dinic 算法
最大流问题中最常用的算法,下面描述该算法的步骤。
- 对原图依据每个点到源点的距离
进行 bfs 分层,暂时删除 的边 。 - 对原图进行 dfs 找到增广路
- 重复上面算法直到源汇点不再连通。
考虑上面算法的正确性:显然。本质上是通过临时删边的方式使得每一次找到一个流使得原图不再连通,而找到流的顺序并不影响最大流的大小。
(6) Dinic 算法的优化
- 无用节点删除:若从一个点
出发无法继续增广,令 ,即变相删除 。 - 当前弧优化:从一个点
增广后可能有很多点 有 ,显然这些点在后续中无用,那么我们每次增广时标记 点之后第一个有用的点记为 ,下次增广时从它开始遍历即可。
经过优化后的 Dinic 时间复杂度 上界 是
我们通常认为 Dinic 的时间复杂度是
。
参考代码:
int cur[N], dep[N];
int bfs() {
for (int i = 1; i <= n; i++)
dep[i] = inf;
queue<int>q;
q.push(s);
cur[s] = head[s];
dep[s] = 0;
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if (e[i].val > 0 && dep[y] == inf) {
q.push(y);
cur[y] = head[y];
dep[y] = dep[x] + 1;
if (y == t)
return 1;
}
}
}
return 0;
}
int dfs(int x, int sum) {
if (x == t)
return sum;
int k, flow = 0;
for (int i = cur[x]; i && sum; i = e[i].nxt) {
int y = e[i].to;
if (e[i].val > 0 && dep[y] == dep[x] + 1) {
k = dfs(y, min(sum, e[i].val));
if (k == 0)
dep[y] = inf;
e[i].val -= k;
e[i ^ 1].val += k;
sum -= k;
flow += k;
}
}
return flow;
}
int dinic() {
int ans = 0;
while (bfs())
ans += dfs(s, inf);
return ans;
}
2. 最大流建模技巧
(1) 拆点
显然
路径问题拆出入点是一个常用的思路。
考虑如何处理“每一秒只能有一个人离开”。将每个门 按照时间 拆成一些个点,把这些个点和汇点
按照时间拆点不常见,但是要掌握这个
(2) 二分图
考虑二分时间,将机器人放在一边,武器放在一边,相互连边描述每一个状态即可。
方格题考虑 黑白染色。黑白染色的目的是把点分为两类,构成一个二分图来网络流。然后按照奇偶性分类,用网络流来
三、最小割问题
1. 最大流最小割定理
(1) 内容
对于任意一张网络,其最大流等于最小割。
(2) 证明
引理:对于一张网络,其任意一个割均
最大流。
证明:假设该网络有一个割
现在考虑如何证明其能取到最大流。
当原图跑到最大流时,原图不存在增广路。那么在最大流的每一个流中总有流量最小的边为 该流的流量限制,这些边流量相加即为最大流,同时这些边也可以组成一个最小割。
于是当我们想求出最小割时,求出原网络最大流即可。
2. 最小割建模技巧
(1) 环式建模
以我所见,最小割的建模通常是环式建模,而环式建模通常有两种。我称一种为“双环式”,另一种为“单环式”,其中“双环式”较为简单暴力,好想但时间复杂度高;“单环式”需要解方程,时间复杂度更优。以我所见,选择“双环式”建模的人更多一些。
两种建模我均选择 [国家集训队] happiness 讲解。这道题的基本思路是先假设全部满足,一边为文科,一边为理科建出二分网络,求出最小割使得每个人只能选择其中一边。
a. 双环建模
双环建模大多数长这个样子:
其中
注意根据题意分析,有时有些边是不用连的。
b. 单环建模
单环建模大多数长这个样子:
我们分别计算切断每组边的损失。令
由题意可以列出方程:
结合不难得到
于是建图时将所有权值全部
(2) 切糕模型
给出
请求出一种合法的赋值方案,使得代价总和最小。
将每个变量
同时对于
3. 最小割的推论
将跑完最大流的残量网络进行缩点,流量为
必然不连通
如果
仍连通,原图上仍有增广路,并未跑满最大流。
- 对于满流边
,若 在同一 SCC, 在同一 SCC,则 必然在原图最小割上。
若
的流量增加,原图重新连通,则最大流流量增加,最小割容量增加,因此 必然出现在最小割中。
- 若
不在同一 SCC, 可能在原图最小割中。
显然原图缩点之后得到的新图只有满流边,原图的任意一个割都是最小割,都可出现在最小割中。
4. 最小割树
求出网络上任意两点的最小割,如果暴力来求,复杂度是
下面介绍最小割树的构建方法。
- 选择任意两个点
,求出最小割,并在新图中将 之间连接一条边权为最小割的边。 - 将整张图的节点分为两部分
集合和 集合,使得割后一部分与 联通,一部分与 联通。 - 分别递归调用与
联通的部分和与 联通的部分,即 集合和 集合。
于是任意两点的最小割,就是它们在最小割树上的简单路径的最小值。具体代码的实现上,将
代码:
#include <iostream>
#include <queue>
#include <unordered_map>
#define N 855
#define M 17505
#define inf 1000000000
using namespace std;
unordered_map<int, int>mp;
int n, m;
struct Netflow {
int s, t;
struct Node {
int to, nxt, val, fval;
}e[M];
int head[N], cnt = 1;
void add(int u, int v, int w) {
e[++cnt].to = v;
e[cnt].fval = w;
e[cnt].nxt = head[u];
head[u] = cnt;
}
void build() {
scanf("%d%d", &n, &m);
for (int i = 1; i <= m; i++) {
int x, y, w;
scanf("%d%d%d", &x, &y, &w);
add(x, y, w);
add(y, x, w);
}
}
int cur[N], dep[N];
int bfs() {
for (int i = 1; i <= n; i++)
dep[i] = inf;
queue<int>q;
q.push(s);
dep[s] = 0;
cur[s] = head[s];
while (!q.empty()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if (e[i].val > 0 && dep[y] == inf) {
q.push(y);
dep[y] = dep[x] + 1;
cur[y] = head[y];
if (y == t)
return 19260817;
}
}
}
return 0;
}
int dfs(int x, int sum) {
if (x == t)
return sum;
int k, flow = 0;
for (int i = cur[x]; i && sum; i = e[i].nxt) {
int y = e[i].to;
if (e[i].val > 0 && dep[y] == dep[x] + 1) {
k = dfs(y, min(e[i].val, sum));
e[i].val -= k;
e[i ^ 1].val -= k;
sum -= k;
flow += k;
}
}
return flow;
}
int dinic() {
for (int i = 2; i <= cnt; i++)
e[i].val = e[i].fval;
int res = 0;
while (bfs())
res += dfs(s, inf);
return res;
}
} Nf;
struct GHT {
int node[N], tmp[N];
int cut[N][N];
void solve(int l, int r) {
if (l == r)
return;
Nf.s = node[l];
Nf.t = node[r];
int k = Nf.dinic(), s = node[l], t = node[r];
cut[s][t] = cut[t][s] = k;
int L = l, R = r;
for (int i = l; i <= r; i++) {
if (Nf.dep[node[i]] < inf)
tmp[L++] = node[i];
else
tmp[R--] = node[i];
}
for (int i = l; i <= r; i++)
node[i] = tmp[i];
solve(l, L - 1);
solve(L, r);
for (int i = l; i < L; i++)
for (int j = L; j <= r; j++) {
cut[node[i]][node[j]] = cut[node[j]][node[i]] = min(min(cut[node[i]][s], cut[node[j]][t]), cut[s][t]);
mp[cut[node[i]][node[j]]] = 1;
}
}
void build() {
for (int i = 1; i <= n; i++)
node[i] = i, cut[i][i] = inf;
solve(1, n);
cout << mp.size() << "\n";
}
} Ght;
int main() {
Nf.build();
Ght.build();
return 0;
}
5. 最大权闭合子图
最大权值闭合图,即给定一张有向图,每个点都有一个权值(可以为正或负或 0),你需要选择一个权值和最大的子图,使得子图中每个点的后继都在子图中。
这个问题可以采用最大流来解决。建立超级源点
考虑证明这个做法。
-
最小割一定是简单割,即与
或 相连的边集组成的割。这一点是显然的。 -
每一个符合条件的子图都对应流量网络中的一个割。钦定闭合子图
和源点 构成 集,其余点和汇点 构成 集。- 证明闭合子图是简单割:若割
不是简单割,则存在 。那么 一定不在 中,矛盾。 - 证明简单割是闭合子图:对于
中任意一个点 , 。 的任意一条出边 ,不会在简单割的割边集中,因此 不属于 ,于是 。所以 的所有点均在 中,因此 构成的边集对应的点是闭合子图。
- 证明闭合子图是简单割:若割
四、费用流
1. 最小费用最大流
显然,一张网络的最大流不一定只有一个。若每条边的单位流量都有费用,考虑寻找一个流使得在流量最大的前提下费用最小。
考虑费用流中反向边的问题。撤回流量的同时费用要同时撤回,因此反向边的费用为正向边的相反数。
显然 对于费用和最小的增广路,我们想让它的流量最大,因此要尽早走。于是我们需要求最短路。考虑到有负边权,我们使用 SPFA。一般地,我们使用 EK 单源增广寻找增广路,因此我们通常使用 EK+SPFA 的费用流。复杂度为
注意求出一条流之后将所有边的流量恢复为原值。
代码:
int dis[N];
int inr[N];
int pre[N];
bool vis[N];
int SPFA() {
for (int i = 1; i <= n; i++) {
dis[i] = inf;
vis[i] = 0;
}
queue<int>q;
q.push(s);
dis[s] = 0;
inr[s] = inf;
vis[s] = 1;
while (!q.empty()) {
int x = q.front();
vis[x] = 0;
q.pop();
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if (e[i].flow > 0 && dis[x] + e[i].val < dis[y]) {
dis[y] = dis[x] + e[i].val;
inr[y] = min(inr[x], e[i].flow);
pre[y] = i;
if (!vis[y])
vis[y] = 1, q.push(y);
}
}
}
return dis[t] < inf;
}
int maxflow, mincost;
void MCMF() {
while (SPFA()) {
maxflow += inr[t];
mincost += inr[t] * dis[t];
int i;
for (int x = t; x != s; x = e[i ^ 1].to) {
i = pre[x];
e[i].flow -= inr[t];
e[i ^ 1].flow += inr[t];
}
}
}
2.费用流常见建模
其实费用流的建模没什么好讲的,掌握好最大流的建模套路其实就差不多了。
五、上下界网络流
1. 无源汇上下界网络流
首先需要明确的是上下界网络并不一定有可行流。首先一个流可行的前提是所有边的流量达到下界,于是先让流满足下界的条件,再考虑将流修改为出入平衡。提升至下界后显然每个点的出/入流量不一定平衡,但净流量相加一定为
代码 of LOJ #115. 无源汇有上下界可行流
#include <bits/stdc++.h>
#define N 205
#define M 100005
#define inf 0x3f3f3f3f
using namespace std;
int n, m;
struct Dinic {
int n, s, t;
struct Node {
int to, nxt, vl;
} e[M];
int head[N], cnt = 1;
int cur[N], dis[N];
void add(int u, int v, int w) {
e[++cnt].to = v;
e[cnt].vl = w;
e[cnt].nxt = head[u];
head[u] = cnt;
}
void ist(int x, int y, int w) {
add(x, y, w);
add(y, x, 0);
}
int bfs() {
for (int i = 1; i <= n; i++) dis[i] = inf;
queue<int>q;
q.push(s);
dis[s] = 0;
cur[s] = head[s];
while (q.size()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if (e[i].vl > 0 && dis[y] == inf) {
dis[y] = dis[x] + 1;
cur[y] = head[y];
q.push(y);
if (y == t) return 1;
}
}
}
return 0;
}
int dfs(int x, int sm) {
if (x == t) return sm;
int k, fl = 0;
for (int i = cur[x]; i && sm; i = e[i].nxt) {
int y = e[i].to;
if (e[i].vl > 0 && dis[y] == dis[x] + 1) {
k = dfs(y, min(sm, e[i].vl));
if (k == 0) dis[y] = inf;
e[i].vl -= k;
e[i ^ 1].vl += k;
fl += k;
sm -= k;
}
}
return fl;
}
int dinic() {
int ans = 0;
while (bfs()) ans += dfs(s, inf);
return ans;
}
} dc;
int l[M], r[M];
int fl[M];
int ans;
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m;
dc.s = n + 1, dc.t = dc.n = n + 2;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y >> l[i] >> r[i];
fl[x] -= l[i];
fl[y] += l[i];
dc.ist(x, y, r[i] - l[i]);
}
for (int i = 1; i <= n; i++) {
if (fl[i] > 0) dc.ist(dc.s, i, fl[i]), ans += fl[i];
else if(fl[i] < 0) dc.ist(i, dc.t, -fl[i]);
}
if (ans == dc.dinic()) {
cout << "YES\n";
for (int i = 1; i <= m; i++) cout << l[i] + dc.e[(i << 1) + 1].vl << "\n";
}
else cout << "NO\n";
return 0;
}
2. 有源汇上下界网络流
(1) 有源汇上下界可行流
考虑有了源汇点之后如何不考虑
(2) 有源汇上下界最大流
最大流的前提是可行,于是先求出一组可行流。由于可行流已经出入平衡,于是在原图的残量网络跑最大流即可,再加上可行流的流量即可。
但是由于
代码 of LOJ #116. 无源汇有上下界可行流
#include <bits/stdc++.h>
#define N 205
#define M 100005
#define inf 0x3f3f3f3f
using namespace std;
int n, m, ans;
struct Dinic {
int n, s, t, S, T;
struct Node {
int to, nxt, vl;
} e[M];
int head[N], cnt = 1;
int cur[N], dis[N];
void add(int u, int v, int w) {
e[++cnt].to = v;
e[cnt].vl = w;
e[cnt].nxt = head[u];
head[u] = cnt;
}
void ist(int x, int y, int w) {
add(x, y, w);
add(y, x, 0);
}
void clr(int x) {
e[x].to = e[x].vl = 0;
}
void del(int x) {
clr(x);
clr(x ^ 1);
}
int bfs(int s, int t) {
for (int i = 1; i <= n; i++) dis[i] = inf;
queue<int>q;
q.push(s);
dis[s] = 0;
cur[s] = head[s];
while (q.size()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if (e[i].vl > 0 && dis[y] == inf) {
dis[y] = dis[x] + 1;
cur[y] = head[y];
q.push(y);
if (y == t) return 1;
}
}
}
return 0;
}
int dfs(int x, int sm, int t) {
if (x == t) return sm;
int k, fl = 0;
for (int i = cur[x]; i && sm; i = e[i].nxt) {
int y = e[i].to;
if (e[i].vl > 0 && dis[y] == dis[x] + 1) {
k = dfs(y, min(e[i].vl, sm), t);
if (k == 0) dis[y] = inf;
e[i].vl -= k;
e[i ^ 1].vl += k;
fl += k;
sm -= k;
}
}
return fl;
}
void dinic() {
int res = 0;
while (bfs(S, T)) res += dfs(S, inf, T);
if (ans != res) cout << "please go home to sleep\n";
else {
ans = e[cnt].vl;
del(cnt);
res = 0;
while (bfs(s, t)) ans += dfs(s, inf, t);
ans -= res;
cout << ans << "\n";
}
}
} dc;
int fl[M], l[M], r[M];
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> dc.s >> dc.t;
dc.S = n + 1, dc.T = dc.n = n + 2;
for (int i = 1; i <= m; i++) {
int x, y;
cin >> x >> y >> l[i] >> r[i];
fl[x] -= l[i];
fl[y] += l[i];
dc.ist(x, y, r[i] - l[i]);
}
for (int i = 1; i <= n; i++) {
if (fl[i] > 0) dc.ist(dc.S, i, fl[i]), ans += fl[i];
else if (fl[i] < 0) dc.ist(i, dc.T, -fl[i]);
}
dc.ist(dc.t, dc.s, inf);
dc.dinic();
return 0;
}
(3) 有源汇上下界最小流
这个东西和最大流是相似的,处理的方法是将多算的流量退回,于是从汇点到源点跑一遍最大流,用可行流减去最大流即可。
代码 of LOJ #117. 无源汇有上下界可行流
#include <bits/stdc++.h>
#define N 50105
#define M 1005005
#define inf 0x3f3f3f3f
using namespace std;
int n, m, ans;
struct Dinic {
int n, s, t, S, T;
struct Node {
int to, nxt, fl;
} e[M];
int head[N], cnt = 1;
void add(int u, int v, int w) {
e[++cnt].to = v;
e[cnt].fl = w;
e[cnt].nxt = head[u];
head[u] = cnt;
}
void ist(int x, int y, int w) {
add(x, y, w);
add(y, x, 0);
}
int dis[N], cur[N];
int bfs(int s, int t) {
for (int i = 1; i <= n; i++) dis[i] = inf;
queue<int>q;
q.push(s);
dis[s] = 0;
cur[s] = head[s];
while (q.size()) {
int x = q.front();
q.pop();
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if (e[i].fl > 0 && dis[y] == inf) {
dis[y] = dis[x] + 1;
cur[y] = head[y];
q.push(y);
if (y == t) return 1;
}
}
}
return 0;
}
int dfs(int x, int sm, int t) {
if (x == t) return sm;
int k, fl = 0;
for (int &i = cur[x]; i && sm; i = e[i].nxt) {
int y = e[i].to;
if (e[i].fl > 0 && dis[y] == dis[x] + 1) {
k = dfs(y, min(e[i].fl, sm), t);
if (k == 0) dis[y] = inf;
e[i].fl -= k;
e[i ^ 1].fl += k;
fl += k;
sm -= k;
}
if (sm == 0) return fl;
}
return fl;
}
void dinic() {
int res = 0;
while (bfs(S, T)) res += dfs(S, inf, T);
if (ans != res) cout << "please go home to sleep\n";
else {
ans = e[cnt].fl;
res = 0;
e[cnt].fl = e[cnt].to = e[cnt ^ 1].fl = e[cnt ^ 1].to = 0;
while (bfs(t, s)) res += dfs(t, inf, s);
ans -= res;
cout << ans << "\n";
}
}
} dc;
int fl[N];
void add(int x, int y, int l, int r) {
fl[x] -= l;
fl[y] += l;
dc.ist(x, y, r - l);
}
int main() {
ios::sync_with_stdio(0);
cin.tie(0);
cin >> n >> m >> dc.s >> dc.t;
dc.S = n + 1, dc.T = dc.n = dc.S + 1;
for (int i = 1; i <= m; i++) {
int x, y, l, r;
cin >> x >> y >> l >> r;
add(x, y, l, r);
}
for (int i = 1; i <= n; i++) {
if (fl[i] > 0) dc.ist(dc.S, i, fl[i]), ans += fl[i];
else if(fl[i] < 0) dc.ist(i, dc.T, -fl[i]);
}
dc.ist(dc.t, dc.s, inf);
dc.dinic();
return 0;
}
(4) 有源汇上下界最小费用可行流
这个东西其实很简单,将所有边的下界乘上费用再跑一遍 MCMF 计入答案即可。
扔一下代码:
struct Dinic {
int n, s, t, S, T;
struct Node {
int to, nxt, fl, dis;
} e[M];
int head[N], cnt = 1;
void add(int u, int v, int fl, int w) {
e[++cnt].to = v;
e[cnt].fl = fl;
e[cnt].dis = w;
e[cnt].nxt = head[u];
// if (cnt == 50) cerr << "K " << u << ' ' << v << '\n';
head[u] = cnt;
}
void ist(int x, int y, int fl, int w) {
add(x, y, fl, w);
add(y, x, 0, -w);
}
int dis[N], inr[N], pre[N];
bool vis[N];
int SPFA(int s, int t) {
for (int i = 1; i <= n; i++) dis[i] = inf, vis[i] = 0;
queue<int>q;
q.push(s);
dis[s] = 0;
inr[s] = inf;
while (q.size()) {
int x = q.front();
q.pop();
vis[x] = 0;
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to;
if (e[i].fl > 0 && dis[y] > dis[x] + e[i].dis) {
dis[y] = dis[x] + e[i].dis;
inr[y] = min(inr[x], e[i].fl);
pre[y] = i;
if (!vis[y]) {
vis[y] = 1;
q.push(y);
}
}
}
}
return dis[t] < inf;
}
void MCMF() {
while (SPFA(S, T)) {
ans += dis[T] * inr[T];
int i;
for (int x = T; x != S; x = e[i ^ 1].to) {
i = pre[x];
e[i].fl -= inr[T];
e[i ^ 1].fl += inr[T];
}
}
}
} dc;
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 单线程的Redis速度为什么快?
· 展开说说关于C#中ORM框架的用法!
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Pantheons:用 TypeScript 打造主流大模型对话的一站式集成库
· SQL Server 2025 AI相关能力初探