网络流
创建时间:2025-03-03
网络的定义
设有向图 \(G=(V,E)\) 为一个网络(network),则 \(G\) 有一个源点(source)和一个汇点(sink),一般,我们将源点称为 \(s\),汇点称为 \(t\)。\(G\) 的每条边有一个边权,表示这条边的容量(capacity),每条边都有一个流量,表示实际通过这条边的流(flow)的大小。
设 \(cap_e\) 为边 \(e\) 的容量, \(flow_e\) 为边 \(e\) 的流量,则 \(G\) 满足
\[\forall e_{(x,y)} \in E, flow_e \le cap_e \] \[\forall v \in V,v \neq s,v \neq t, \sum x_{(u,v)} flow_x = \sum y_{(v,w)} flow_y \]第1条被称为网络的“流量限制”,第2条被称为网络的“流量守恒”。
最大流
最大流的定义
对于任意网络,称其最大流(maxflow)为其源点到汇点的流量的最大值,即
\[maxflow = \sum e_{(s,x)} flow_e = \sum e_{(y,t)} flow_e \]
EK算法求解最大流
EK(Edmonds–Karp)的思想很简单,就是不断地通过BFS寻找一条长度最小的增广路\(^{\tiny{\textcircled{\scriptsize{1}}}}\),再更新增广路上所有边的边权(即还能通过的流量)和最大流的答案,直到 \(G\) 中不存在增广路。
可以发现,直接进行EK算法会遇到一个问题:之前的残余网络\(^{\tiny{\textcircled{\scriptsize{2}}}}\) 在当时是最优解,但不一定在后续也是最优解。为了解决这个问题,我们可以建立一个反悔机制:为每条边建立反向边,使正向边的边权+反向边边权=原边权。这样当我们当我们需要更改残余网络以确保答案最优性时,便可以先找到反边,再通过反边找到正边,使用如减少正边边权,增加反边边权的方式直接更改。
如何通过反边找到正边?这里需要用到一个技巧:使用链式前向星存边,将初始编号设为1(第一条边的编号为2),在建完正边后立即建反边,这样有正边编号1=反边编号,反边编号1=正边编号,如21=3,32=1。
以上便是EK算法的大体思路,具体实现如下:
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Edge {
int from, to, nxt;
ll w;
};
const ll INF = 1e18;
const int MAX_N = 250;
const int MAX_M = 5050;
int n, m, s, t, pre[MAX_N];
int head[MAX_N], tot = 1;
Edge e[MAX_M * 2];
bool vis[MAX_N];
ll flow[MAX_N], maxflow;
void add_edge(int u, int v, ll w) {
e[++tot] = Edge{u, v, head[u], w};
head[u] = tot;
e[++tot] = Edge{v, u, head[v], 0};
head[v] = tot;
}
bool bfs() {
fill(vis + 1, vis + n + 1, false);
queue<int> q;
flow[s] = INF;
vis[s] = true;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (e[i].w && !vis[v]) {
vis[v] = true;
q.push(v);
pre[v] = i;
flow[v] = min(flow[u], e[i].w);
if (v == t)
return true;
}
}
}
return false;
}
void update() {
int cur = t;
while (cur != s) {
e[pre[cur]].w -= flow[t];
e[pre[cur] ^ 1].w += flow[t];
cur = e[pre[cur]].from;
}
maxflow += flow[t];
}
int main() {
cin >> n >> m >> s >> t;
for (int u, v, w; m--; ) {
cin >> u >> v >> w;
add_edge(u, v, w);
}
while (bfs())
update();
cout << maxflow;
return 0;
}
注:
- 增广路,即一条从 \(s\) 到 \(t\) 的路径,该路径上的所有边均还有边权,跑通增广路可以使最大流答案增加。
- 残余网络,指在经过一段操作后剩余的网络,其中每条边的边权表示还能通过它的流量大小。
- EK的时间复杂度为 \(O(nm^2)\),虽劣于Dinic的 \(O(n^2m)\),但也已经相当优秀了,且思维和代码难度远低于Dinic。
Dinic算法求解最大流
Dinic的思路与EK相似,但EK一次BFS只能找到一条增广路,Dinic一次能找到多条增广路。
Dinic和EK一样,都是通过BFS判断残余网络中是否还有增广路,但Dinic再BFS的同时对网络进行了分层,并使用DFS一次找到多条增广路。
具体实现如下:
const int INF = 0x3f3f3f3f;
const int MAX_N = 1000;
const int MAX_M = MAX_N * MAX_N;
int n, m, s, t, sum;
vector<int> p[MAX_N];
int sc[MAX_N], indg[MAX_N];
int head[MAX_N], tot = 1;
struct edge {
int to, nxt, w;
} e[MAX_M * 2];
int dep[MAX_N], now[MAX_N];
void add_edge(int u, int v, int w) {
e[++tot] = edge{v, head[u], w};
head[u] = tot;
e[++tot] = edge{u, head[v], 0};
head[v] = tot;
}
bool bfs() {
memset(dep, 0, sizeof(dep));
memcpy(now, head, sizeof(now));
queue<int> q;
dep[s] = 1;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop();
for (int i = head[u]; i; i = e[i].nxt) {
int v = e[i].to;
if (!dep[v] && e[i].w) {
dep[v] = dep[u] + 1;
q.push(v);
if (v == t)
return true;
}
}
}
return false;
}
int dfs(int u, int sum) {
if (u == t)
return sum;
int flow = 0;
for (int i = now[u]; i && sum; i = e[i].nxt) {
now[u] = i;
int v = e[i].to, &w = e[i].w;
if (dep[v] == dep[u] + 1 && w) {
int tmp = dfs(v, min(sum, w));
if (tmp == 0) {
dep[v] = 0;
continue;
}
w -= tmp, e[i ^ 1].w += tmp;
sum -= tmp, flow += tmp;
}
}
return flow;
}
int Dinic() {
int maxflow = 0;
while (bfs())
maxflow += dfs(s, INF);
return maxflow;
}
注:
- 不管是EK还是Dinic,最大流算法的时间复杂度一般都跑不满,Dinic在处理层数少的网络时复杂度甚至接近 \(O(n^2)\)。
最大流的应用
最大流建模的题,其每条边通常都有一个限制,且限制一般是流量或选择次数,又或是类似二分图最大匹配的问题;当限制在点上时,可以使用拆点的技巧,例:
洛谷P2065(边数过多时的最大流);
洛谷P2763(最大流建模+拆点);
洛谷P2472(最大流建模+拆点);
洛谷P2766(DP+最大流建模+拆点)。
最小割
最小割的定义
对于有向图 \(G=(V,E)\),割掉边权总和最少的边,使 \(G\) 中点 \(s\) 无法到达点 \(t\)。
或者说,令 \(w_i\) 为边 \(i\) 的边权,找到集合 \(S \subseteq E\),满足删去 \(S\) 中所有边后\(s\) 无法到达 \(t\),且 \(\sum_{e \in S},w_e\) 最小。
最小割的实现
对于有向图 \(G\),我们可以先将 \(G\) 转化为一个网络,当 \(G\) 中存在增广路时,我们必须将这条路径割断,而割掉的那条边肯定是边权最小的那条边,不难发现,这条边权最小的边的边权恰好是这条增广路的流量!
故有 \(G\) 的最小割等于 \(G\) 的最大流。
具体实现同最大流,这里不再展示。
最小割的应用
最小割建模中 \(s\) 到 \(t\) 的一条路径通常表示一种矛盾,而最小割正是要让每个人做出“二选一”或“选不选”的决定,将矛盾“割掉”,例:
洛谷P2057(最小割建模);
洛谷P2774(最小割建模);
洛谷P4313(最小割建模+虚拟节点);
洛谷P1646(最小割建模+虚拟节点)。
最大权闭合子图
最大权闭合子图的定义
对于带点权的有向图 \(G\)(点权可以为负),找到点权总和最大的子图,满足该子图中所有节点的出边均在该子图中。
最大权闭合子图的实现
对于点权为正的点,将超级源点 \(s\) 向其连边,边权为其点权,表示在最大权闭合子图中选择这个点;其他的点,向超级汇点 \(t\) 连边,边权为其点权的绝对值,表示在最大权闭合子图中不选这个点;原图中的边,在网络中正常连边即可,边权为 \(\infty\)。
这样,当 \(s\) 到 \(t\) 存在路径时,一定有节点 \(x\) 在最大权闭合子图中,且存在 \(e_{(x,y)}\) 满足 \(y\) 不在最大权闭合子图中,这与最大权闭合子图的定义相矛盾!故要么从最大权闭合子图中将 \(x\) 踢出,即隔断 \(s\) 到 \(x\) 的边,或将 \(y\) 加入最大权闭合子图,即隔断 \(y\) 到 \(t\) 的边。
所以,我们可以通过最小割实现最大闭合子权图,其答案=正点权总和-最小割答案。
最大权闭合子图的应用
最大权闭合子图的建模常常表现在依赖关系,即选择某个点必须选择其他的几个点,例:
洛谷P3410(最大权闭合子图建模);
CF1082G(最大权闭合子图建模);
洛谷P2762(最大权闭合子图建模+输出方案);
洛谷P2805(最大权闭合子图建模+拓扑排序);
洛谷P3749(最大权闭合子图建模)。
费用流
费用流的定义
顾名思义,对于网络 \(G\),其每条边都有一个费用(cost) \(c\),当大小为 \(f\) 的流流过这条边时会产生 \(f \cdot c\) 的费用。
最小费用最大流的实现
最小费用最大流,即在满足流量最大的前提下最小化费用。
有费用流的定义得,一条增广路上产生的费用为流量 \(f * \sum e_{(u,v,w,c)} c\),所以我们可以贪心的每次寻找 \(\sum e_{(u,v,w,c)} c\) 最小的增广路,即 \(s\) 到 \(t\) 的最短路,这与先前最大流BFS的过程极为相像,故只需要将BFS替换成最短路算法并修改update函数即可。
那么应该选用怎样的最短路算法呢?考虑之前的反悔机制,显然,在费用流中,我们应该将反边的边权设为 \(-c\),所以我们便需要可以处理负权值的最短路算法(SPFA、bellman-ford等)。
这样便得到了最小费用最大流的解法,代码如下。
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll INF = 1e18;
const int MAX_N = 5e3 + 50;
const int MAX_M = 5e4 + 50;
int n, m, s, t;
int head[MAX_N], tot = 1;
struct Edge {
int from, to, nxt;
ll w, c;
} e[MAX_M * 2];
int pre[MAX_N];
ll dis[MAX_N], flow[MAX_N];
ll maxflow, mincost;
bool inq[MAX_N];
void add_edge(int u, int v, ll w, ll c) {
e[++tot] = Edge{u, v, head[u], w, c};
head[u] = tot;
e[++tot] = Edge{v, u, head[v], 0, -c};
head[v] = tot;
}
bool SPFA() {
fill(dis + 1, dis + n + 1, INF);
dis[s] = 0, flow[s] = INF;
queue<int> q;
q.push(s);
while (!q.empty()) {
int u = q.front();
q.pop(), inq[u] = false;
for (int i = head[u]; i && flow[u]; i = e[i].nxt) {
int v = e[i].to;
if (e[i].w && dis[v] > dis[u] + e[i].c) {
dis[v] = dis[u] + e[i].c;
flow[v] = min(flow[u], e[i].w);
pre[v] = i;
if (!inq[v])
q.push(v), inq[v] = true;
}
}
}
return dis[t] != INF;
}
void update() {
int cur = t;
while (cur != s) {
e[pre[cur]].w -= flow[t];
e[pre[cur] ^ 1].w += flow[t];
mincost += flow[t] * e[pre[cur]].c;
cur = e[pre[cur]].from;
}
maxflow += flow[t];
}
int main() {
cin >> n >> m >> s >> t;
for (int u, v, w, c; m--; ) {
cin >> u >> v >> w >> c;
add_edge(u, v, w, c);
}
while (SPFA())
update();
cout << maxflow << ' ' << mincost;
return 0;
}
注:
- 在求解最小费用最大流时,EK和Dinic的时间复杂度差距较小,但EK代码短,思路更简单,所以一般情况下用EK求解最小费用最大流。
费用流的应用
费用流通常是要求最小费用最大流,一般在满足某种边的限制且某个量取到极值的前提下求最小/最大费用,例:
洛谷P4452(最小费用最大流建模+拆点);
洛谷P2517(最小费用最大流建模);
洛谷P2053(最小费用最大流建模+拆点);
洛谷P4553(最小费用最大流建模+拆点+强行满流处理);
洛谷P3159(最小费用最大流建模+拆点+贪心最优策略);

浙公网安备 33010602011771号