网络流入门
网络流是什么?#
网络流是 OI 图论中的内容,比较有用的是网络最大流与费用流。
网络流就像一个复杂的水网,它有一个源点
比如下图,图中的边就相当于水管,边上的数字(权值)就是每条边的流量上限。
这是将水输送到
其中紫色数字是水流大小
那这个图的的流量就是
找阻塞流#
如果我们不能从源点输送更多的水到汇点,那么这个水流的状态就叫阻塞流。
上图就不是一个阻塞流,因为实际上可以从源点往容量为
我们还要知道:
空闲量 = 容量 - 流量
即空闲量表示还可容纳多少水。
首先,我们建出以空闲量为权值的图
然后我们随便从图
取到这条路径上最小的权值
然后再将所没用的边去掉,即容量为
不断重复上述步骤,
当我们已经找不到路径了,我们就可以停止了。
我们把刚刚减去
网络最大流#
什么是网络最大流呢?
就是在不超过容量的情况下,从源点输送更多的水到汇点。
比如上图的网络最大流就是
那么我们怎么求网络最大流呢?
有很多算法可以办到,比如 Ford–Fulkerson,EK,dinic 算法。
可以这么理解 EK 是 Ford–Fulkerson 的优化,dinic 是 EK 的优化。
Ford–Fulkerson 算法#
首先,我们发现再刚刚找阻塞流的时候就是一个劲地往下找,
运气不好就找不到网络最大流,比如下面这个例子(注意不是上面那个图),
找到的阻塞流为
其实,我们可以建立一条反向边。
让水倒着流回去,比如:
那么我们在找阻塞流的时候顺便加上一条容量相同的反向边就实现了“可以后悔”的做法,
即找错了边也不要紧,再回来不就完了。
算法过程就变成了:
- 随便找一条从
到 的路径,与找阻塞流一样; - 然后取该边上最小的权值
,让所有的边都减去 ,与找阻塞流一样; - 建立一条权值为
的反边,可以让水流原路流回去。
比如下图,图中虚线是反边,红色边是找到的路径(正反边皆可),紫色数字是反边权值,黑色数字是原边权值:
然后不断重复以上步骤,直到找不到路径为止。
最后我们把刚刚得到的所有
Ford–Fulkerson 算法的时间复杂度为
EK 算法#
剩下的部分,终于不用画图了,纯属优化
EK 算法唯一优化的地方是找路径的时候找最短路径,而不是随便找,BFS 就可以实现。
当然,我们是把图当作无权图来对待。
发明者证明了该算法时间复杂度为
C++代码:#
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 210, M = 10010, inf = 0x3f3f3f3f3f3f3f3f;
struct Edge {
int to;
int next;
LL c;
} e[M];
int head[N], idx = 1;
void add(int a, int b, int c) {
idx++;
e[idx].next = head[a];
e[idx].c = c;
e[idx].to = b;
head[a] = idx;
}
int n, m, S, T;
bool vis[N];
int q[N];
int hh, tt;
int pre[N];
LL flow[N];
bool bfs() {
memset(vis, 0, sizeof vis);
vis[S] = 1;
hh = 0, tt = -1;
q[++tt] = S;
flow[S] = inf;
while (hh <= tt) {
int t = q[hh++];
for (int i = head[t]; i; i = e[i].next) {
if (e[i].c) {
int to = e[i].to;
if (vis[to]) continue;
flow[to] = min(flow[t], e[i].c);
vis[to] = 1;
pre[to] = i;
q[++tt] = to;
if (to == T) return 1;
}
}
}
return 0;
}
LL ans;
void update() {
int x = T;
while (x != S) {
int p = pre[x];
e[p].c -= flow[T];
e[p ^ 1].c += flow[T];
x = e[p ^ 1].to;
}
ans += flow[T];
}
int main() {
scanf("%d%d%d%d", &n, &m, &S, &T);
for (int i = 1; i <= m; i++) {
int a, b;
LL c;
scanf("%d%d%lld", &a, &b, &c);
add(a, b, c);
add(b, a, 0);
}
while (bfs()) update();
printf("%lld\n", ans);
return 0;
}
dinic 算法#
我们在找最短路径的时候顺便将图分层,从
比如:
然后我们在找路径的时候,加入我们已经到了
其他内容与 EK 算法相同,算法发明者证明了其时间复杂度为
C++代码:#
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 210, M = 10010, INF = 1e16;
struct edge {
int to, next, w;
} e[M];
int head[N], idx = 1;
void add(int a, int b, int c) {
idx++, e[idx].to = b, e[idx].next = head[a], e[idx].w = c, head[a] = idx;
idx++, e[idx].to = a, e[idx].next = head[b], e[idx].w = 0, head[b] = idx;
}
int n, m, S, T;
int q[N];
int hh, tt;
int d[N];
bool bfs() {
memset(d, 0, sizeof(d));
hh = tt = 0;
q[0] = S;
d[S] = 1;
while (hh <= tt) {
int t = q[hh++];
for (int i = head[t]; i; i = e[i].next) {
int to = e[i].to;
if (!d[to] && e[i].w) {
d[to] = d[t] + 1;
q[++tt] = to;
if (to == T) return true;
}
}
}
return false;
}
int dinic(int u, int limit) {
if (u == T) return limit;
int rest = limit;
for (int i = head[u]; i && rest; i = e[i].next) {
int to = e[i].to;
if (d[to] == d[u] + 1 && e[i].w) {
int k = dinic(to, min(rest, e[i].w));
if (!k) d[to] = 0;
rest -= k;
e[i].w -= k;
e[i ^ 1].w += k;
}
}
return limit - rest;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> S >> T;
for (int i = 1; i <= m; i++) {
int a, b, c;
cin >> a >> b >> c;
add(a, b, c);
}
int maxflow = 0, flow = 0;
while (bfs()) {
while (flow = dinic(S, INF)) {
maxflow += flow;
}
}
cout << maxflow << '\n';
return 0;
}
费用流#
思路#
有时,输送水是需要付钱的!
费用流就是在每条管道指定要解决在保证最大流的前提下,付的钱最少。
每条边会有两个权值,
那么怎么解决这个问题呢?
实际上,非常简单,既然要保证最大流,那么还是需要找路径,又要付的钱最少,
那么就相当于每一条边带上了一个权值,我们不能再把它当作无权图来对待了。
您可能已经想到,可以跑最短路,这样既找到路径,又保证费用最小化。
是的,费用流非常简单,只要把网络最大流的 BFS 改成 SPFA 即可,
至于为什么不用 Dijkstra,很显然可能有负边权,但一般遇不到,对这一点有疑惑的可以看看官方抽象的定义。
那我们怎么建反边呢?
容量与最大流一样,就是正边流过了
但是价格不太一样,假如正边的价格是
此时,我们因为用了 SPFA,那么说明费用流的算法就不是基于分层图的,
因为如果像最大流一样 BFS,那么 BFS 到哪一层,哪一层的点一定是最先被访问到的,一定是最短的,
但是加上了权值就不一样了,经过同一层次的点也有可能最短,所以费用流的算法就不是基于分层图的。
我们用 dinic 反而是小题大作,还要分个层,所以我们费用流多用 EK 算法。
C++代码#
#include <bits/stdc++.h>
using namespace std;
const int N = 5010, M = 100010, INF = 0x3f3f3f3f;
struct edge {
int to, next, w, cost;
} e[M];
int head[N], idx = 1;
void add(int u, int v, int w, int cost) {
idx++;
e[idx].to = v;
e[idx].next = head[u];
e[idx].w = w;
e[idx].cost = cost;
head[u] = idx;
idx++;
e[idx].to = u;
e[idx].next = head[v];
e[idx].w = 0;
e[idx].cost = -cost;
head[v] = idx;
}
int n, m, S, T;
int dis[N], pre[N], flow[N];
bool st[N];
bool spfa() {
queue<int> q;
q.push(S);
memset(dis, 0x3f, sizeof(dis));
memset(flow, 0, sizeof(flow));
dis[S] = 0;
flow[S] = INF;
st[S] = true;
while (q.size()) {
int t = q.front();
q.pop();
st[t] = false;
for (int i = head[t]; i; i = e[i].next) {
int to = e[i].to;
if (e[i].w && dis[to] > dis[t] + e[i].cost) {
dis[to] = dis[t] + e[i].cost;
pre[to] = i;
flow[to] = min(flow[t], e[i].w);
if (!st[to]) {
q.push(to);
st[to] = true;
}
}
}
}
return flow[T] > 0;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> S >> T;
for (int i = 1; i <= m; i++) {
int u, v, w, cost;
cin >> u >> v >> w >> cost;
add(u, v, w, cost);
}
int maxflow = 0, mincost = 0;
while (spfa()) {
maxflow += flow[T];
mincost += flow[T] * dis[T];
int x = T;
while (x != S) {
e[pre[x]].w -= flow[T];
e[pre[x] ^ 1].w += flow[T];
x = e[pre[x] ^ 1].to;
}
}
cout << maxflow << ' ' << mincost << '\n';
return 0;
}
经典例题#
SP4063 MPIGS - Sell Pigs / P4638 [SHOI2011] 银行家题解#
考虑使用网络流。
建立源点
每个人作为一个点,将它们与汇点
然后对于每个人,如果和之前的某个人开了相同的猪圈,那么就将之前的那个人的点与这个人的点连接。
如果猪圈还没有被开过,就从源点
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 2510, M = 1000010, INF = 0x3f3f3f3f3f3f3f3f;
struct edge {
int to, next, w;
} e[M];
int head[N], idx = 1;
void add(int u, int v, int w) {
idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
idx++, e[idx].to = u, e[idx].next = head[v], e[idx].w = 0, head[v] = idx;
}
int n, m, S, T;
int dep[N];
bool bfs() {
queue<int> q;
q.push(S);
memset(dep, 0x3f, sizeof(dep));
dep[S] = 1;
while (q.size()) {
int t = q.front();
q.pop();
for (int i = head[t]; i; i = e[i].next) {
int to = e[i].to;
if (dep[to] > dep[t] + 1 && e[i].w) {
dep[to] = dep[t] + 1;
q.push(to);
}
}
}
if (dep[T] < INF) return true;
else return false;
}
int dinic(int u, int limit) {
if (u == T) return limit;
int rest = limit;
for (int i = head[u]; i && rest; i = e[i].next) {
int to = e[i].to;
if (dep[to] == dep[u] + 1 && e[i].w) {
int k = dinic(to, min(rest, e[i].w));
if (!k) dep[to] = INF;
rest -= k;
e[i].w -= k;
e[i ^ 1].w += k;
}
}
return limit - rest;
}
int last[N], init[N];
signed main() {
freopen("a.in", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> m >> n;
for (int i = 1; i <= m; i++) cin >> init[i];
S = 0, T = n + 1;
for (int i = 1; i <= n; i++) {
int cnt;
cin >> cnt;
while (cnt--) {
int x;
cin >> x;
if (last[x]) add(last[x], i, INF);
else add(S, i, init[x]);
last[x] = i;
}
int x;
cin >> x;
add(i, T, x);
}
int flow = 0, maxflow = 0;
while (bfs()) while (flow = dinic(S, INF)) maxflow += flow;
cout << maxflow << '\n';
return 0;
}
P2754 [CTSC1999] 家园 / 星际转移问题题解#
开始时,将源点连一条权值为
然后以时间分层,从上一层的点连接到下一层的点,权值为飞船载人数量,并将代表月球的点连接到汇点。每加一层,在上一层的基础上进行增广,看能不能增加流量,如果流量变为
可以证明枚举时间最多到
不用担心超时,luogu上最慢才 31 ms
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 22500, M = 10000010, INF = 0x3f3f3f3f3f3f3f3f;
struct edge {
int to, next, w;
} e[M];
int head[N], idx = 1;
void add(int u, int v, int w) {
idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
idx++, e[idx].to = u, e[idx].next = head[v], e[idx].w = 0, head[v] = idx;
}
int S, T;
int dep[N];
bool bfs() {
queue<int> q;
q.push(S);
memset(dep, 0x3f, sizeof(dep));
dep[S] = 1;
while (q.size()) {
int t = q.front();
q.pop();
for (int i = head[t]; i; i = e[i].next) {
int to = e[i].to;
if (dep[to] > dep[t] + 1 && e[i].w) {
dep[to] = dep[t] + 1;
q.push(to);
}
}
}
if (dep[T] < INF) return true;
else return false;
}
int dinic(int u, int limit) {
if (u == T) return limit;
int rest = limit;
for (int i = head[u]; i && rest; i = e[i].next) {
int to = e[i].to;
if (dep[to] == dep[u] + 1 && e[i].w) {
int k = dinic(to, min(rest, e[i].w));
if (!k) dep[to] = INF;
rest -= k;
e[i].w -= k;
e[i ^ 1].w += k;
}
}
return limit - rest;
}
int n, m, k;
int h[N], r[N];
vector<int> a[N];
int turn(int x, int c) {
return c * (n + 2) + x;
}
signed main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> m >> k;
for (int i = 1; i <= m; i++) {
cin >> h[i];
cin >> r[i];
a[i].resize(r[i]);
for (auto& x : a[i]) {
cin >> x;
if (x == 0) x = n + 1;
else if (x == -1) x = n + 2;
}
}
S = 0, T = N - 1;
add(S, turn(n + 1, 0), k);
int maxflow = 0;
for (int c = 1; c <= 7; c++) {
// cout << "first" << n + 2 << ' ' << c << endl;
add(turn(n + 2, c), T, INF);
for (int i = 1; i <= n + 1; i++) add(turn(i, c - 1), turn(i, c), INF);
for (int i = 1; i <= m; i++) {
int lst = ((c - 1) % r[i] + r[i]) % r[i];
int cur = c % r[i];
// cout << a[i][lst] << ' ' << c - 1 << endl;
// cout << a[i][cur] << ' ' << c << endl;
add(turn(a[i][lst], c - 1), turn(a[i][cur], c), h[i]);
}
int flow = 0;
while (bfs()) while (flow = dinic(S, INF)) maxflow += flow;
if (maxflow == k) {
cout << c << '\n';
return 0;
}
}
cout << 0 << '\n';
return 0;
}
P1402 酒店之王题解#
考虑使用网络流。
分为
第一层为源点。
第二层为所有菜的点。
第三层和第四层都表示人。(限制只能选择一个)。
第五层为房子。
第六层为汇点。
#include <bits/stdc++.h>
#define int long long
using namespace std;
const int N = 410, M = 101000, INF = 0x3f3f3f3f3f3f3f3f;
int n, m, S, T;
struct edge {
int to, next, w;
} e[M];
int head[N], idx = 1;
void add(int u, int v, int w) {
// cout << u << ' ' << v << ' ' << w << endl;
idx++, e[idx].to = v, e[idx].next = head[u], e[idx].w = w, head[u] = idx;
idx++, e[idx].to = u, e[idx].next = head[v], e[idx].w = 0, head[v] = idx;
}
int dep[N];
bool bfs() {
queue<int> q;
q.push(S);
memset(dep, 0x3f, sizeof(dep));
dep[S] = 1;
while (q.size()) {
int t = q.front();
q.pop();
for (int i = head[t]; i; i = e[i].next) {
int to = e[i].to;
if (dep[to] > dep[t] + 1 && e[i].w) {
dep[to] = dep[t] + 1;
q.push(to);
}
}
}
return dep[T] < INF;
}
int dinic(int u, int limit) {
if (u == T) return limit;
int rest = limit;
for (int i = head[u]; i && rest; i = e[i].next) {
int to = e[i].to;
if (dep[to] == dep[u] + 1 && e[i].w) {
int k = dinic(to, min(rest, e[i].w));
if (!k) dep[to] = INF;
rest -= k;
e[i].w -= k;
e[i ^ 1].w += k;
}
}
return limit - rest;
}
int p, q;
signed main() {
// freopen("a.in", "r", stdin);
ios::sync_with_stdio(false);
cin.tie(nullptr);
S = 0, T = N - 1;
cin >> n >> p >> q;
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= p; j++) {
int x;
cin >> x;
if (x) add(j, i + 100, 1);
}
}
for (int i = 1; i <= n; i++) {
for (int j = 1; j <= q; j++) {
int x;
cin >> x;
if (x) add(i + 200, j + 300, 1);
}
}
for (int i = 1; i <= 100; i++) add(i + 100, i + 200, 1);
for (int i = 1; i <= 100; i++) add(S, i, 1);
for (int i = 301; i <= 400; i++) add(i, T, 1);
int maxflow = 0, flow = 0;
while (bfs()) while (flow = dinic(S, INF)) maxflow += flow;
cout << maxflow << '\n';
return 0;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具