【Coel.学习笔记】【梦开始的地方】网络流的基本概念与最大流算法
这个暑假要好好学一下省选内容了,争取明年进队!
先从网络流开始……
网络流的基本概念
网络流的定义非常繁多且复杂,而且对于最大流、最小割的学习十分重要。
网络(流网络)是一张有向图 ,起始点 为源点,终点 为汇点,其中每条边的权值 被称为边的容量。
为了定义方便,我们认为这张图中没有反向边。如果的确存在反向边,那么可以给反方向的边加上中间点。
每条边还有一个值 称为可行流。可行流有如下性质:
- 容量限制:
- 流量守恒:对于非汇点、源点的点,流入该点的流量之和等于流出该点的流量之和。
- 斜对称:在某些教材中认为存在反向边,此时有另一个性质 。
净流量 定义为流出流量减去流入流量。
最大流又称最大可行流,是使得整个网络流量最大的可行流取值。
残留网络(残量网络/残余网络) 也是一张有向图,其点为流网络中的所有点,边为流网络中所有的正向边和反向边。
残留网络的容量定义:
- 这条边为原图的正向边,则容量为 ,即可以增加的流量;
- 这条边为原图的反向边,容量为 ,即可以返回的流量。
残留网络也存在可行流 ,满足残留网络可行流加上原网络可行流也是原网络的一个可行流。
流的加法为正向相加,反向相减。根据这一点,可以得到性质 。
增广路径(增广路):对于一个网络中,从源点到汇点,并且可行流的值均大于零的路径。
割:把点集分成不重叠的两部分 ,使得源点与汇点在不同的两个集合之中。
割的容量指所有从 到达 的边的容量之和,记为 。相应地,最小割为使得容量 最小的割。
割的流量 为所有从 到 的流入流量减去流出流量。割的流量一定不大于割的容量,且有 。割的流量同样有斜对称 ,以及两个集合之间的割流量等于其中一个集合的两个对立子集分别对应另一个集合的割流量。
关于 的证明:
.
最大流最小割定理:以下三个条件可知一推二:
- 可行流对应的残余网络不存在增广路;
- 存在某一个割 使得可行流的割容量等于最大流的流量;
- 这个可行流是最大流。
由此可知,最大流等于最小割。
最大流算法
Ford-Fulkerson 方法(FF 方法)
Ford-Fulkerson 是大多数最大流算法的基本思想:维护网络的残留网络,每次迭代在当前残留网络中找增广路,不断更新得到的残留网络。EK 算法和 Dinic 算法都是基于这个思想方法的。
Edmonds-Karp(EK)增广路算法
在 EK 算法中,找增广路径采用 bfs 解决,更新残留网络则利用正向边容量与反向边容量之间的变化来计算。
为了更新残留网络,我们需要用一个链表存储下增广路。这里利用到一个“成对存储”的方法,使得正向边与反向边一一配对,这时对于一条边 pre[i]
,可以直接用 pre[i] ^ 1
得到反向边。
EK 算法的理论时间复杂度为 ,但上界极其宽松,通常可以胜任 到 级别的数据量。
参考代码如下:
// Problem: P3376 【模板】网络最大流
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3376
// Memory Limit: 128 MB
// Time Limit: 2000 ms
// Algorithm: Edmonds_Karp
// Powered by CP Editor (https://cpeditor.org)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
using namespace std;
#define int long long // 本题的 w < 2^31...
const int maxn = 2e5 + 10, inf = 1e9;
int n, m, s, t;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int d[maxn], pre[maxn]; //d[u] 为从源点到达点 u 的所有流量最小值
bool vis[maxn];
void add(int u, int v, int w) { // 对残余网络建图,不理解的话请回顾定义
to[cnt] = v, c[cnt] = w, nxt[cnt] = head[u], head[u] = cnt++;
to[cnt] = u, c[cnt] = 0, nxt[cnt] = head[v], head[v] = cnt++;
//反向边开始没有流量
}
bool bfs() {
memset(vis, 0, sizeof(vis));
queue<int> Q;
Q.push(s), vis[s] = true;
d[s] = inf;
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for (int i = head[u]; i != -1; i = nxt[i]) {
int v = to[i];
if (!vis[v] && c[i] > 0) { //容量大于零,找增广路
vis[v] = true;
d[v] = min(d[u], c[i]);
pre[v] = i;
if (v == t) //到达汇点,结束算法
return true;
Q.push(v);
}
}
}
return false;
}
int Edmonds_Karp() {
int ans = 0;
while (bfs()) {
ans += d[t]; //维护答案
for (int i = t; i != s; i = to[pre[i] ^ 1]) { //维护残余网络容量
c[pre[i]] -= d[t];
c[pre[i] ^ 1] += d[t]; //配对存储法
}
}
return ans;
}
signed main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
memset(head, -1, sizeof(head));
cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
cout << Edmonds_Karp();
return 0;
}
Dinic 算法
EK 算法在每次迭代中可能只找到一条增广路,在特定图下表现可能变差。
Dinic 算法进行了一个优化,即在每次搜索时尝试找到能多增广路。
但直接使用这个优化,当原图存在环时会出现问题,所以采用了分层图的方法,且要求每次找到的增广路必须从本层走到下一层,避免环的出现。具体地,找增广路时找到图的层次,然后做 dfs 找出所有能增广的路径并统一增广。
除此之外,Dinic 算法还引入了当前弧优化,如果某条边的容量已满,那么下次迭代就不会进行增广。
Dinic 算法的时间复杂度为 ,同样上界很宽松,且效率比 EK 算法快很多,是最为常用的最大流算法。
代码如下:
// Problem: P3376 【模板】网络最大流
// Contest: Luogu
// URL: https://www.luogu.com.cn/problem/P3376
// Memory Limit: 128 MB
// Time Limit: 2000 ms
// Algorithm: Dinic
// Powered by CP Editor (https://cpeditor.org)
#include <iostream>
#include <cstring>
#include <algorithm>
#include <queue>
#define int long long
using namespace std;
const int maxn = 2e5 + 10, inf = 1e9;
int n, m, s, t;
int head[maxn], nxt[maxn], to[maxn], c[maxn], cnt;
int d[maxn], cur[maxn]; //d 为图的层次, cur 用于当前弧优化
void add(int u, int v, int w) {
nxt[cnt] = head[u], to[cnt] = v, c[cnt] = w, head[u] = cnt++;
nxt[cnt] = head[v], to[cnt] = u, c[cnt] = 0, head[v] = cnt++;
}
bool bfs() {
queue<int> Q;
memset(d, -1, sizeof(d));
Q.push(s), d[s] = 0, cur[s] = head[s];
while (!Q.empty()) {
int u = Q.front();
Q.pop();
for (int i = head[u]; i != -1; i = nxt[i]) {
int v = to[i];
if (d[v] == -1 && c[i]) { //未被找到层次,更新
d[v] = d[u] + 1;
cur[v] = head[v]; //以首边为当前弧
if (v == t) return true;
Q.push(v);
}
}
}
return false;
}
int find(int u, int limit) {
if(u == t)
return limit;
int flow = 0;
for (int i = cur[u]; i != -1 && flow < limit; i = nxt[i]) {
cur[u] = i; //当前弧优化
int v = to[i];
if (d[v] == d[u] + 1 && c[i]) {
int tem = find(v, min(c[i], limit - flow));
if (!tem) d[v] = -1;
c[i] -= tem, c[i ^ 1] += tem; //同样的配对存储
flow += tem;
}
}
return flow;
}
int Dinic() {
int ans = 0, flow;
while (bfs())
while ((flow = find(s, inf)))
ans += flow;
return ans;
}
signed main(void) {
ios::sync_with_stdio(false);
cin.tie(nullptr);
memset(head, -1, sizeof(head));
cin >> n >> m >> s >> t;
for (int i = 1; i <= m; i++) {
int u, v, w;
cin >> u >> v >> w;
add(u, v, w);
}
cout << Dinic();
return 0;
}
本文作者:Coel's Blog
本文链接:https://www.cnblogs.com/Coel-Flannette/p/16464000.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步