网络流 笔记
本文原在 2024-07-22 10:17 发布于本人洛谷博客。
一、定义与性质
1. 基本定义
从水厂出发,有很多节点和水管,节点不能存水,但容量无限,水管有容量上限,全部水管最终经过某些节点都会流向某个工厂里,问最多同时能给工厂发多少水?
流网络:这张图。
源点:水厂。
汇点:工厂。
弧:水管。
弧的流量:这条水管当前流多少水,对于弧 \((u,v)\) 用 \(f(u,v)\) 表示。
弧的容量:这条水管最多能流多少水,用 \(c(u,v)\) 表示。
弧的残量:这条水管还能流多少水,即 \(c(u,v)-f(u,v)\)。
流量网络/容量网络/残量网络:边权表示/流量/容量/残量的流网络。
(重要) 增广路:在残量网络中,还能从源点流向汇点的一条路径。
2. 性质
-
斜对称性:\(f(u,v)=-f(v,u)\)。
-
流量守恒:
- 对于节点,输出量和输入量相等。
- 源点的输出量等于汇点的输入量。
二、最大流
1. EK 算法
第一章第 1 节中所提到的问题就属于最大流问题。
基本思路就是:建一张容量网络,找到一条路径就给每条边减去相应的流量,并在剩下的残量网络上,不断找增广路。
但是对于下面这个图,显然找不到增广路了,但是肉眼可见最大流是 \(1\to 2\to 4\) 加上 \(1\to 3\to 4\)。
因此给程序一个反悔的机会:给每条边都建一条反向边(根据 \(0\oplus 1 =1\),\(1\oplus 0 = 0\);\(2\oplus 1 = 3\),\(3\oplus 1 = 2\);\(4\oplus 1 =5\)……的性质,可以用 \(i\) 和 \(i\oplus 1\) 作为一组相反的边),边权表示正向边的流量,实时更新。
每次找到一条路径后,给路径上的边减去本路径的最大流,路径上的反边加上本路径的最大流。这样就会发现:如果路径中含有一条反向边,它恰好能将正向边的容量给推回去。
int bfs() { // bfs寻找增广路 for (int i = 1; i <= n; i++) vis[i]=0; queue<int> q; q.push(s); vis[s] = 1; dis[s] = oo; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[x]; i; i = edge[i].next) { if (edge[i].flow == 0) continue; // 如果是正向边:没有残量了怎么流?如果是反向边:正向边没有流量怎么流? int v = edge[i].v; if (vis[v]) continue; // 访问过了 flow[v] = min(flow[u], edge[i].flow); // 显然,一条路径的最大流取决于能流最小的那条边 pre[v] = i; // 记录前驱,方便修改边权 q.push(v); vis[v] = 1; if (v == t) return 1; // 找到了一条增广路 } } return 0; } void update() { // 更新所经过边的正向边权以及反向边权 int u = t; while (u != s) { int v = pre[u]; edge[v].flow -= flow[t]; edge[v ^ 1].flow += flow[t]; u = edge[v ^ 1].v; } maxflow += flow[t]; // 累加每一条增广路经的最小流量值 } void EK() { while (bfs()) update(); }
2. Dinic 算法
EK 算法一次才找一条增广路,使用 Dinic 算法的 DFS 可以一次寻找多条增广路。
用 BFS 对图分层,作用在于一次可以得到多条长度相同的最短增广路。
每次从当前点出发,选用从当前点所在层到下一层的边,发送一定的流量,流量的大小取边残量和当前点从源点获取的剩余流中两者的最小值。
搜索完成后,返回一个流量值,即这条增广路的流量,此时就能够对边和反向边的残量进行更新了。
bool bfs() { fill(dis, dis + N, oo); queue<int> q; q.push(s); dis[s] = 0; now[s] = head[s]; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i; i = edge[i].next) { int v = edge[i].v; if (edge[i].w <= 0 or dis[v] != oo) continue; q.push(v); now[v] = head[v]; // 记录当前弧优化,可以不写,直接用 head dis[v] = dis[u] + 1; // 分层 if (v == t) // 到汇点了 return true; } } return false; } int dfs(int u, int last) { if (u == t) return last; // 到汇点了 int ret = 0; for (int i = now[u]; i and last; i = edge[i].next) { now[u] = i; int v = edge[i].v; if (edge[i].w <= 0 or dis[v] != dis[u] + 1) // 没有流量或者层数对不上 continue; int tmp = dfs(v, min(last, edge[i].w)); // 往后走的最大流 if (!tmp) dis[v] = oo; edge[i].w -= tmp; edge[i ^ 1].w += tmp; ret += tmp; last -= tmp; } return ret; } void dinic() { while(bfs()) ans += dfs(s, oo); }
三、费用流
以最小费用最大流为例:
每条水管要收费,假设水管 \((u,v)\) 的单价是 \(cost\),那么你就要付 \(f(u,v)\times cost\)。
将 EK 算法的 BFS 改成 SPFA 即可,可能有负边不能 Dijkstra。
namespace McMf { int cost[N], flow[N], pre[N]; bool vis[N]; bool spfa() { queue<int> q; fill(cost, cost + N, oo); memset(vis, 0, sizeof vis); q.push(s); cost[s] = 0; vis[s] = true; flow[s] = oo; while (!q.empty()) { int u = q.front(); q.pop(); vis[u] = false; for (int i = head[u]; i; i = edge[i].next) { if (!edge[i].flow) continue; int v = edge[i].v; if (cost[v] > cost[u] + edge[i].cost) { cost[v] = cost[u] + edge[i].cost; flow[v] = min(flow[u], edge[i].flow); pre[v] = i; if (!vis[v]) { q.push(v); vis[v] = true; } } } } return cost[t] <= oo / 2; } void mcmf() { while (spfa()) { int u = t; maxflow += flow[u]; mincost += flow[u] * cost[u]; while (u != s) { int p = pre[u]; edge[p].flow -= flow[t]; edge[p ^ 1].flow += flow[t]; u = edge[p ^ 1].v; } } } } using namespace McMf;
四、上下界网络流
1. 无源汇上下界可行流
假设上下界是 \([L,R]\),点 \(x\) 流入的量是 \(in_x\),流出的量是 \(out_x\)。
令每条边都先流自己的 \(L\),统计到 \(in_x\) 和 \(out_x\) 中,然后构造一个流网络,边权为 \(R-L\)。
如果 \(in_x>out_x\),那么建边 \((s,x,in_x-out_x)\)。
如果 \(in_x<out_x\),那么建边 \((x,t,out_x-in_x)\)。
在这个新构造的流网络中,如果源点流出的每一条边都能流满,那么说明流量平衡。
2. 有源汇上下界可行流
连接题目给出的源点 \((t,s,[0,\infty])\),用 1 的方法。
3. 有源汇上下界最大流
跑完可行流后,在残量网络中找 \(s\to t\) 的最大流。
#include <bits/stdc++.h> #define int long long #define IOS ios::sync_with_stdio(0), cin.tie(0), cout.tie(0) using namespace std; const int N = 1e6 + 10, oo = 1e18; int n, m, s, t, s1, t1, in[N], out[N], over; int head[N], ide = 1; struct EDGE { int v, next, w; } edge[N]; void add(int u, int v, int w) { edge[++ide] = {v, head[u], w}; head[u] = ide; edge[++ide] = {u, head[v], 0}; head[v] = ide; } int dis[N], now[N]; bool bfs() { fill(dis, dis + N, oo); queue<int> q; q.push(s); dis[s] = 0; now[s] = head[s]; while (!q.empty()) { int u = q.front(); q.pop(); for (int i = head[u]; i; i = edge[i].next) { int v = edge[i].v; if (edge[i].w <= 0 or dis[v] != oo) continue; q.push(v); now[v] = head[v]; dis[v] = dis[u] + 1; if (v == t) return true; } } return false; } int dfs(int u, int last) { if (u == t) return last; int ret = 0; for (int i = now[u]; i and last; i = edge[i].next) { now[u] = i; int v = edge[i].v; if (edge[i].w <= 0 or dis[v] != dis[u] + 1) continue; int tmp = dfs(v, min(last, edge[i].w)); if (!tmp) dis[v] = oo; edge[i].w -= tmp; edge[i ^ 1].w += tmp; ret += tmp; last -= tmp; } return ret; } int dinic() { int ret = 0; while(bfs()) ret += dfs(s, oo); return ret; } signed main() { IOS; cin >> n >> m >> s1 >> t1; s = n + 1, t = n + 2; for (int i = 1, u, v, lb, ub; i <= m; i++) { cin >> u >> v >> lb >> ub; in[v] += lb, out[u] += lb; add(u, v, ub - lb); } for (int i = 1; i <= n + 2; i++) if (in[i] > out[i]) { add(s, i, in[i] - out[i]); over += in[i] - out[i]; } else add(i, t, out[i] - in[i]); add(t1, s1, oo); if (dinic() != over) { cout << "please go home to sleep"; return 0; } s = s1, t = t1; cout << dinic(); return 0; }
4. 有源汇上下界最小流
求出可行流,把 \((t,s,[0,\infty])\) 删掉,再跑一次,前后两次相减。
本文作者:Garbage fish's Blog
本文链接:https://www.cnblogs.com/Garbage-fish/p/18709926
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步