网络流
网络流基础
基本概念
- 网络流 (NetWork Flow) : 一种类比水流的解决问题的方法。
(下述概念均会用水流进行解释) - 网络 (NetWork) : 可以理解为拥有源点和汇点的有向图。
(运输水流的水管路线路) - 弧 (arc) : 可以理解为有向边。下文均用 “边” 表示。
(水管) - 弧的流量 (Flow) : 简称流量。在一个流量网络中每条边都会有一个流量,表示为 f(x,y) ,根据流函数 f 的定义,f(x,y) 可为负。
(运输的水流量) - 弧的容量 (Capacity) : 简称容量。在一个容量网络中每条边都会有一个容量,表示为 c(x,y) 。
(水管规格。即可承受的最大水流量) - 源点 (Sources) : 可以理解为起点。它会源源不断地放出流量,表示为 S 。
(可无限出水的 NB 水厂) - 汇点 (Sinks) : 可以理解为终点。它会无限地接受流量,表示为 T 。
(可无限收集水的 NB 小区) - 容量网络: 拥有源点和汇点且每条边都给出了容量的网络。
(安排好了水厂,小区和水管规格的路线图) - 流量网络: 拥有源点和汇点且每条边都给出了流量的网络。
(分配好了各个水管水流量的路线图) - 弧的残留容量: 简称残留容量。在一个残量网络中每条边都会有一个残留容量 。对于每条边,残留容量 = 容量 − 流量。初始的残量网络即为容量网络。
(表示水管分配了水流量后还能继续承受的水流量) - 残量网络: 拥有源点和汇点且每条边都有残留容量的网络。残量网络 = 容量网络 − 流量网络。
(表示了分配了一定的水流量后还能继续承受的水流量路线图)
(表示了分配了一定的水流量后还能继续承受的水流量路线图)
基本性质
- 流量: 在某种方案下形成的流量网络中汇点接收到的流量值。
(小区最终接收到的总水流量) - 最大流: 网络的流量的最大值。
(小区最多可接受到的水流量) - 网络: 达到最大流的流量网络。
(使得小区接收到最多水流量的分配方案路线图)
最大流
概念补充
- 网络的流量: 在某种方案下形成的流量网络中汇点接收到的流量值。
(小区最终接收到的总水流量) - 最大流: 网络的流量的最大值。
(小区最多可接受到的水流量) - 最大流网络: 达到最大流的流量网络。
(使得小区接收到最多水流量的分配方案路线图)
增广路算法(EK)
性质补充
增广路定理 (Augmenting Path Theorem): 流量网络达到最大流当且仅当残量网络中没有增广路。
(无法再找到一路线使得小区获得更多的流量了)
算法详解
根据 增广路定理 , 我们可以每次 \(Bfs\) 一条新的增广路, 接着将这条增广路上灌上水. 此处新增流量应是该路上 所有残留容量的最小值(木桶定理) .
在实际解决基本问题时, 我们只要记录残留容量即可.
但是, 单纯这样做方法可能不是最优, 因此我们有时需要反悔, 所以我们要建反向图. 初始的反向边残留容量为 \(0\), 若正向边残留容量减 \(x\), 则反向边残留容量加 \(x\), 就可以直接处理了(想一想为什么).
Code
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define Maxn 200
#define Maxm 5000
#define Inf 0x3f3f3f3f3f3f3f3f
#define int long long
#define reverse(x) ((x & 1) ? (x + 1) : (x - 1))
struct EDGE {int to, flow, nxt;};
int n, m, so, si, heads[Maxn + 9], cnt, head, tail, q[Maxn + 9], mini[Maxn + 9], pre[Maxn + 9], ans;
bool vis[Maxn + 9];
EDGE e[(Maxm << 1) + 9];
int read() {
int f = 1, sum = 0;
char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') sum = (sum << 3) + (sum << 1) + ch - '0', ch = getchar();
return f * sum;
}
void write(int x) {
if(x < 0) putchar('-'), write(-x);
else if(x <= 9) putchar(x + '0');
else write(x / 10), putchar(x % 10 + '0');
}
void Add(int u, int v, int w) {
e[++cnt].nxt = heads[u], e[cnt].to = v, e[cnt].flow = w, heads[u] = cnt;
}
bool Bfs(int start, int end) {
memset(vis, 0, sizeof(vis));
head = 1, tail = 1;
q[tail] = start, vis[start] = 1, mini[start] = Inf;
while(head <= tail) {
int u = q[head++];
for(int i = heads[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].flow;
if(!vis[v] && w) {
mini[v] = min(mini[u], w);
q[++tail] = v, vis[v] = 1, pre[v] = i;
if(v == end) return 1;
}
}
}
return 0;
}
void EK(int start, int end) {
while(Bfs(start, end)) {
ans += mini[end];
int now = end;
while(now != start) {
int i = pre[now];
e[i].flow -= mini[end];
e[reverse(i)].flow += mini[end];
now = e[reverse(i)].to;
}
}
}
signed main() {
n = read(), m = read(), so = read(), si = read();
for(int i = 1; i <= m; ++i) {
int u = read(), v = read(), w = read();
Add(u, v, w), Add(v, u, 0);
}
EK(so, si);
write(ans), puts("");
return 0;
}
Dinic
观察 \(EK\) 算法, 我们发现每次只找到了一条增广路, 效率明显可以提升, 于是, 我们考虑每次 \(Bfs\) 寻找多条增广路. 于是, 我们就得到了 \(Dinic\) 算法.
算法流程
\(Bfs\) 原图, 在 \(EK\) 的基础上多给每个点分配一个"深度", 即被遍历的层数. 然后进行一遍 \(Dfs\), 走的下一个节点的深度必须是当前节点的深度 \(+1\). \(Dfs\) 回溯时返回最终收到的水流量更新当前节点的信息.
当前弧优化
\(Dfs\) 时, 每一条走过的路必定已被水填满, 故不再需要遍历. 因此, 我们在每次 \(Dfs\) 时跳过以遍历过的路.
Code
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
#define Maxn 200
#define Maxm 5000
#define int long long
#define Inf 0x3f3f3f3f3f3f3f3f
#define reverse(x) ((x & 1) ? (x + 1) : (x - 1))
struct EDGE {int to, nxt, flow;};
int n, m, cnt, heads[Maxn + 9], ans, cur[Maxn + 9], q[Maxn + 9], head, tail, level[Maxn + 9];
EDGE e[(Maxm << 1) + 9];
int read() {
int f = 1, sum = 0;
char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') sum = (sum << 3) + (sum << 1) + ch - '0', ch = getchar();
return f * sum;
}
void write(int x) {
if(x < 0) putchar('-'), write(-x);
else if(x <= 9) putchar(x + '0');
else write(x / 10), putchar(x % 10 + '0');
}
void Add(int u, int v, int w) {
e[++cnt].nxt = heads[u], e[cnt].to = v, e[cnt].flow = w, heads[u] = cnt;
}
bool Bfs(int start, int end) {
memset(level, 0, sizeof(level));
head = tail = 1, level[start] = 1, q[tail] = start;
bool Flag = 0;
while(head <= tail) {
int u = q[head++];
for(int i = heads[u]; i; i = e[i].nxt) {
int v = e[i].to, w = e[i].flow;
if(!level[v] && w) {
level[v] = level[u] + 1, q[++tail] = v;
if(v == end) Flag = 1;
}
}
}
return Flag;
}
int Dfs(int u, int end, int flow) {
if(!flow || u == end) return flow;
int temp = 0;
for(int i = cur[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(level[v] == level[u] + 1) {
cur[u] = i;
int w = Dfs(v, end, min(flow - temp, e[i].flow));
e[i].flow -= w, e[reverse(i)].flow += w;
temp += w;
if(temp == flow) break;
}
}
return temp;
}
void Dinic(int start, int end) {
while(Bfs(start, end)) {
for(int i = 1; i <= n; ++i) cur[i] = heads[i];
ans += Dfs(start, end, Inf);
}
}
signed main() {
n = read(), m = read();
int start = read(), end = read();
for(int i = 1; i <= m; ++i) {
int u = read(), v = read(), w = read();
Add(u, v, w), Add(v, u, 0);
}
Dinic(start, end);
write(ans), puts("");
return 0;
}
最大流模板
最小割
割
如果在一个网络中, 删去若干条弧后从源点出发不能再到达汇点, 那么则称删去的这些弧为该网络的一个割.
最小割
设删去每个弧所需代价为其容量, 则最小割即为代价最小的割集.
最大流最小割定理
在一个网络中, 最大流等于最小割.
费用流
每一条弧有一个单位流量的费用, 即从该弧中经过需要花费 单位流量的费用\(\times\)流量 的价钱. 费用流, 即最小费用最大流, 顾名思义, 保证流量最大的情况下使得花费最小.
EK
算法流程
在最大流的基础上, 将 \(Bfs\) 改为 \(SPFA\), 边长设为单位流量的费用.
注: \(SPFA\) 不能用 \(Dijkstra\) 代替, 因为为了能够反悔, 反向边为单位流量的费用的相反数, 为 负数.
Code
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
#define Maxn 5000
#define Maxm 50000
#define Inf 0x3f3f3f3f3f3f3f3f
#define int long long
#define reverse(x) ((x & 1) ? (x + 1) : (x - 1))
struct EDGE {int to, flow, w, nxt;};
int n, m, so, si, cnt, heads[Maxn + 9], ansflow, anscost, mini[Maxn + 9], dis[Maxn + 9], pre[Maxn + 9];
bool vis[Maxn + 9];
queue<int> Q;
EDGE e[(Maxm << 1) + 9];
int read() {
int f = 1, sum = 0;
char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') sum = (sum << 3) + (sum << 1) + ch - '0', ch = getchar();
return f * sum;
}
void write(int x) {
if(x < 0) putchar('-'), write(-x);
else if(x <= 9) putchar(x + '0');
else write(x / 10), putchar(x % 10 + '0');
}
void Add(int u, int v, int flow, int w) {
e[++cnt].nxt = heads[u], e[cnt].to = v, e[cnt].flow = flow, e[cnt].w = w, heads[u] = cnt;
}
bool SPFA(int start, int end) {
memset(dis, 0x3f, sizeof(dis)), memset(vis, 0, sizeof(vis));
Q.push(start), dis[start] = 0, vis[start] = 1, mini[start] = Inf;
while(!Q.empty()) {
int u = Q.front();
Q.pop(), vis[u] = 0;
for(int i = heads[u]; i; i = e[i].nxt) {
int v = e[i].to, flow = e[i].flow, w = e[i].w;
if(flow && dis[v] > dis[u] + w) {
dis[v] = dis[u] + w, pre[v] = i;
mini[v] = min(mini[u], flow);
if(!vis[v]) vis[v] = 1, Q.push(v);
}
}
}
if(dis[end] == Inf) return 0;
return 1;
}
void EK(int start, int end) {
while(SPFA(start, end)) {
ansflow += mini[end], anscost += mini[end] * dis[end];
int now = end;
while(now != start) {
int i = pre[now];
e[i].flow -= mini[end];
e[reverse(i)].flow += mini[end];
now = e[reverse(i)].to;
}
}
}
signed main() {
n = read(), m = read(), so = read(), si = read();
for(int i = 1; i <= m; ++i) {
int u = read(), v = read(), flow = read(), w = read();
Add(u, v, flow, w), Add(v, u, 0, -w);
}
EK(so,si);
write(ansflow), putchar(' ');
write(anscost), puts("");
}
Dinic
算法流程
同最大流的 \(Dinic\), 将 \(Bfs\) 改为 \(SPFA\) 即可.
Code
#include<cstdio>
#include<cstring>
#include<queue>
#include<algorithm>
using namespace std;
#define Maxn 5000
#define Maxm 50000
#define Inf 0x3f3f3f3f3f3f3f3f
#define int long long
#define reverse(x) ((x & 1) ? (x + 1) : (x - 1))
struct EDGE {int to, flow, w, nxt;};
int n, m, so, si, cnt, heads[Maxn + 9], ansflow, anscost, dis[Maxn + 9], cur[Maxn + 9];
bool vis[Maxn + 9];
queue<int> Q;
EDGE e[(Maxm << 1) + 9];
int read() {
int f = 1, sum = 0;
char ch = getchar();
while(ch < '0' || ch > '9') {if(ch == '-') f = -1; ch = getchar();}
while(ch >= '0' && ch <= '9') sum = (sum << 3) + (sum << 1) + ch - '0', ch = getchar();
return f * sum;
}
void write(int x) {
if(x < 0) putchar('-'), write(-x);
else if(x <= 9) putchar(x + '0');
else write(x / 10), putchar(x % 10 + '0');
}
void Add(int u, int v, int flow, int w) {
e[++cnt].nxt = heads[u], e[cnt].to = v, e[cnt].flow = flow, e[cnt].w = w, heads[u] = cnt;
}
bool SPFA(int start, int end) {
memset(dis, 0x3f, sizeof(dis));
Q.push(start), dis[start] = 0, vis[start] = 1;
while(!Q.empty()) {
int u = Q.front();
Q.pop(), vis[u] = 0;
for(int i = heads[u]; i; i = e[i].nxt) {
int v = e[i].to, flow = e[i].flow, w = e[i].w;
if(flow && dis[v] > dis[u] + w) {
dis[v] = dis[u] + w;
if(!vis[v]) vis[v] = 1, Q.push(v);
}
}
}
if(dis[end] == Inf) return 0;
return 1;
}
int Dfs(int u, int end, int flow) {
if(u == end) {
ansflow += flow;
anscost += dis[u] * flow;
return flow;
}
int temp = 0;
vis[u] = 1;
for(int i = cur[u]; i; i = e[i].nxt) {
int v = e[i].to;
if(dis[v] == dis[u] + e[i].w && e[i].flow && !vis[v]) {
cur[u] = i;
int fflow = Dfs(v, end, min(flow - temp, e[i].flow));
e[i].flow -= fflow, e[reverse(i)].flow += fflow;
temp += fflow;
if(temp == flow) break;
}
}
vis[u] = 0;
return temp;
}
void Dinic(int start, int end) {
while(SPFA(start, end)) {
for(int i = 1; i <= n; ++i) cur[i] = heads[i];
Dfs(start, end, Inf);
}
}
signed main() {
n = read(), m = read(), so = read(), si = read();
for(int i = 1; i <= m; ++i) {
int u = read(), v = read(), flow = read(), w = read();
Add(u, v, flow, w), Add(v, u, 0, -w);
}
Dinic(so,si);
write(ansflow), putchar(' ');
write(anscost), puts("");
}
最小费用最大流模板
例题
题单
P2057 [SHOI2007] 善意的投票 / [JLOI2010] 冠军调查
这道题可以用两个点表示支持睡的以及支持不睡的, 然后谁支持这个观点就与其相连, 再将好朋友之间相连, 求最小割即可.
P1646 [国家集训队]happiness
用快乐值总和减去最小代价.
首先套路地, 从源点 \(s\) 向每个点连选文科的价值, 从每个点向汇点 \(t\) 连选理科的价值, 割哪条表示不选哪科, 这样可以保证每个人不选文就选理.
若有一对好朋友中有一人没有选文科(理科同理), 我们就要减去他们都选文科的代价(理科同理). 我们考虑对于每一组朋友, 新建一个节点连向 \(s\), 容量为同选文科的快乐值, 然后由 \(x\) 向这对朋友连两条容量为正无穷的边.
因为当他选完一科后, 连向另一科的边就全断掉了, 故保证了每个人只选一科的情况.
P2053 [SCOI2007] 修车
若第 \(i\) 个工人修的 \(k\) 辆车用时为 \(T_{1-k}\), 则总等待时间为 \(T_1+T_1+T_2+T_1+T_2+T_3+...+T_k\), 所以他修的倒数第 \(k\) 辆车贡献为 $k \times $ 原所需时间.
所以, 构造二分图, 左侧为待修的车, 右侧为\(n \times n \times m\) 个点, 每个点用(i, j, k)表示, 即第 \(i\) 个工人将第 \(j\) 辆车放到倒数第 \(k\) 个修. 费用为 \(k \times\) 原时间. 然后连源点、汇点, 跑一遍最小费用最大流即可.

浙公网安备 33010602011771号