算法学习笔记(8.1): 网络最大流算法 EK, Dinic, ISAP
网络最大流
前置知识以及更多芝士参考下述链接
网络流合集链接:网络流
最大流,值得是在不超过管道(边)容量的情况下从源点到汇点最多能到达的流量
抽象一点:使 \(\sum_{(S, v) \in E} f(S, v)\) 最大的流函数被称为网络的最大流,此时的流量被称为网络的最大流量
有了最大流量,就可以通过奇奇怪怪的建模解决很多令人摸不着头脑的题
例如二分图:
对于一张二分图,经过建模之后我们可以这样画
其中左部点集 \(A = \{1, 2, 3, 4\}\),右部点集 \(B = \{5, 6, 7, 8\}\), 其中源点为 \(0\),汇点为 \(9\)
建模过程:
新增一个源点 \(S\) 和一个汇点 \(T\), 从 \(S\) 到每一个左部点连有向边,从每一个右部点到 \(T\) 连有向边,把原二分图的每条边看作从左部点到右部点的有向边,形成了一张 \(n + 2\) 个点 \(n + m\) 条边的网络。其中每一条边的容量都为 \(1\)。
不难发现,二分图的最大匹配数就等于网络的最大流量。求出最大流后,所有有有”流“经过的点,边就是匹配点,匹配边。
进一步的:如果要求二分图多重匹配,依据题目信息改变连接汇点和源点的边的容量即可
计算最大流的算法很多,这里主要讲解 \(EK (Edmonds-Karp)\) ,\(Dinic\) 和 \(ISAP\) 算法。
EK 增广路算法
这里的增广路与二分图里面的增广路有不一样了 Q^Q
增广路:若一条源点 \(S\) 到 \(T\) 的路径上各边的剩余容量都严格大于 \(0\),则称这条路径为增广路。显然,可以让一个流沿着增广路流过,使得网络的流量增大。
而\(EK\)的思路就是不断进行广度优先搜索寻找增广路,直到不存在增广路为止。
而在搜索的时候,我们只考虑图中所有 \(f(x, y) < c(x,y)\) 的边(或者说是有剩余容量的边)。在寻找路径的同时,还要记录其前驱结点以及路径上的最小容量\(minf\), 在找到一条增广路后,则网络的流量可以增加 \(minf\)。
但是,考虑到斜对称的性质,由于我们需要把增广路上的所有边的剩余容量 \(e(u, v)\) 减去\(minf\),所以要在对其反边容量 \(e(v, u)\) 加上 \(minf\)。
初始化的时候,如果是有向边 \((u, v)\),则 \(e(u, v) = c(u, v), e(v, u) = 0\),如果是无向边,则 \(e(u, v) = e(v, u) = c(u, v)\)。
?: 为什么会出现无向边,网络流不是有向图吗?
考虑双向道路马路,既可以顺流,又可以逆着。
例如:[ICPC-Beijing 2006] 狼抓兔子 - 洛谷
这道题需要用到最小割,顺便说一下,最小割 = 最大流
?: 为什么使用BFS,而不是DFS?
因为DFS可能会绕圈圈……在讲述DInic的时候我会再提及
复杂度:复杂度上界为 \(O(nm^2)\),然而实际上远远达不到这个上界,效率还行,可以处理 \(10^3 \sim 10^4\) 规模的网络
--《算法竞赛进阶指南》
我不会证明,下面的两个算法也不会 Q^Q
这里给出一种参考代码
提交记录:记录详情
#include <iostream>
#include <cstring>
#include <algorithm>
#include <deque>
using std::deque;
const int N = 2e3 + 7, M = 5e5 + 7, INF = 0x7F7F7F7F;
int n, m, s, t;
int to[M], nex[M], wi[M] = {INF};
int head[N], tot = 1;
void add(int u, int v, int w) {
to[++tot] = v, nex[tot] = head[u], wi[tot] = w, head[u] = tot;
}
void read() {
scanf("%d %d %d %d", &n, &m, &s, &t);
int u, v, w;
for (int i = 0; i < m; ++i) {
scanf("%d %d %d", &u, &v, &w);
add(u, v, w);
add(v, u, 0);
}
}
#define min(x, y) ((x)<(y)?(x):(y))
#define pop() que.pop_front()
#define top() que.front()
#define push(x) que.push_back(x);
#define empty() que.empty()
static int inq[N], it = 0;
// px记录上一个点,pe记录遍历过来的边
static int px[N], pe[N];
inline int bfs() {
deque<int> que;
push(s); inq[s] = ++it;
int x, y;
while (!empty()) {
x = top(); pop();
for (int i = head[x]; i; i = nex[i]) {
if ((inq[(y = to[i])] ^ it) && wi[i]) {
px[y] = x, pe[y] = i;
if (y == t) return 1; // 找到增广路了,
inq[y] = it; push(y);
}
}
}
return 0; // 到不到了,没有增广路了
}
void work(long long & res) {
while (bfs()) {
int val = INF;
for (int x = t; x ^ s; x = px[x]) {
val = min(val, wi[pe[x]]);
}
for (int x = t; x ^ s; x = px[x]) {
wi[pe[x]] -= val;
// 处理反边的时候利用了成对变换的方法!
wi[pe[x] ^ 1] += val;
}
res += val;
}
}
int main() {
read();
long long res = 0;
work(res);
printf("%lld\n", res);
return 0;
}
Dinic
考虑到 \(EK\) 算法每一次在残量网络上只找出来的一条增广路,太慢了,所以有了更优化的东西 Dinic?歌姬吧
先引入一点点概念:
深度:在搜索树上的深度(BFS搜索时的层数)
残量网络:网络中所有节点以及剩余容量大于 \(0\) 的边构成的子图
分层图:依据深度分层的一段段图……或者说在残量网络上,所有满足 \(dep[u] + 1 = dep[v]\) 的边 \((u, v)\) 构成的子图。
分层图显然是一张有向无环图
Dinic 算法不断重复下述过程,直到在残量网络中,\(S\) 不能到达 \(T\)
利用BFS求出分层图
在分层图上DFS寻找增广路,在回溯的时候实时更新剩余容量。另外,每个点可以同时流出到多个结点,每个点也可以接收多个点的流。
?: 这里为什么可以使用DFS
由于我们分了层,意味着DFS只会向更深的地方搜索,而不会在同一层乱跳,甚至搜索到前面。这也是为什么EK用BFS更优秀
复杂度:一般来说,时间复杂度为 \(O(n^2m)\),可以说是不仅简单,而且容易实现的高翔算法之一,一般能够处理 \(10^4 \sim 10^5\) 规模的网络。特别的,用此算法求二分图的最大匹配时只需要 \(O(m\sqrt{n})\), 实际上表现会更好。
题目不变
没有当前弧优化:提交详情
有当前弧优化:记录详情
// 重复内容已省略
int dis[N], vis[N], vt = 0;
int now[N]; // 用于当前弧优化
// return true if exists non-0 road to t
bool bfs() {
memset(dis, 0, sizeof(dis)); dis[s] = 1;
deque<int> que;
que.push_back(s);
while (que.size()) {
int x = que.front(); que.pop_front();
now[x] = head[x]; // 更新当前弧
for (int y, i = head[x]; i; i = nex[i]) {
if (!dis[y = to[i]] && wi[i]) {
dis[y] = dis[x] + 1;
que.push_back(y);
if (y == t) return true;
}
}
}
return false;
}
#define min(x, y) ((x) < (y) ? (x) : (y))
long long dinic(int x, long long maxflow) {
if (x == t) return maxflow;
long long rest = maxflow, k;
for (int y, i = now[x]; i && rest; i = nex[i]) {
now[x] = i; // 更新当前弧
// 要在更深的一层,以及需要有剩余流量
if (dis[y = to[i]] == dis[x] + 1 && wi[i]) {
k = dinic(y, min(rest, wi[i]));
if (!k) dis[y] = 0;
wi[i] -= k, wi[i ^ 1] += k;
rest -= k;
}
}
return maxflow - rest;
}
int main() {
read();
long long maxflow = 0, flow;
while (bfs()) {
while (flow = dinic(s, INF)) maxflow += flow;
}
printf("%lld\n", maxflow);
}
?: 当前弧优化是个啥玩意
注意到如果我们每一次遍历后,对于当前边 \((u, v)\),不可能再有流量流过这条边,所以我们可以暂时的删除这条边……注意,只是暂时,每一分层的时候是需要考虑这条边的,因为这条边的剩余流量不一定为 0
ISAP
某位大佬的博客上说这是究极最大流算法之一。还有一个HLPP(最高标记预留推进),思路完全与这几个方法不同,不依赖于增广路,我会把它放在另外的文章中单独讲。我可不会告诉你们是我不会优化,太笨了,看不懂大佬的优化
这是我的:记录详情 4.77s
这是大佬的:记录详情 185ms
由于Dinic需要多次BFS……所以有些不满足的数学家决定优化常数……于是有了ISAP,只需要一次BFS的东西……
可恶,竟然没有找到不用gap优化的写法 T^T
ISAP算法从某种程度上是SAP算法和Dinic的融合
SAP算法就是所谓的EK算法……ISAP也就是Improved SAP……但是主体怎么跟DInic几乎一模一样!
算法流程如下:
从 \(T\) 开始进行BFS,直到 \(S\) ,标记深度,同时记录当前深度有多少个
利用DFS不断寻找增广路,思路与Dinic类似
每次回溯结束后,将所在节点深度加一(远离 \(T\) 一点),同时更新深度记录。如果出现了断层(有一个深度没有点了)那么结束寻找。
?: 为什么需要深度加一
由于我们在便利过一次过后,这个点不可能再向更靠近 \(T\) 的点送出流量,所以只能退而求其次,给自己同层的结点送流量。
怎么跟Dinic一摸一样啊,关键是也可以用当前弧优化,只是我用写的是vetor存图……用不了
参考代码……
提交题目还是【模板】网络最大流 - 洛谷
!! 竟然在最优解第二页 O-O
对于下面代码做出一些解释
?: 为什么终止条件是
dep[s] > n
考虑如果是
dep[s] <= n
的情况,由于只有n个点,意味着只要最大深度 \(\lt n\) 那么要么是有连续的层,要么断层了(此时我们在DFS中会将dep[s]设为n+1
如果最大深度 \(\gt n\) 所以一定会有一个深度是没有点的,意味着一定出现了断层,也就是流量无法到达了
所以,更新答案之后就可以结束循环了
// 写这个的时候,借鉴了写HLPP最优解的大佬写快读的方法……
template<typename T>
inline void read(T &x) {
char c, f(0); x = 0;
do if ((c = getchar()) == '-') f = true; while (isspace(c));
do x = (x<<3) + (x<<1) + (c ^ 48), c = getchar(); while (isdigit(c));
if (f) x = -x;
}
template <typename T, typename ...Args> inline void read(T &t, Args&... args) { read(t), read(args...); }
typedef long long Data;
using namespace std;
const int N = 207, M = 5007;
struct Edge {
int to;
size_t rev; // 反边的位置,用int也没问题
Data flow;
Edge(int to, size_t rev, Data f) : to(to), rev(rev), flow(f) {}
};
class ISAP {
public:
int n, m, s, t;
vector<int> dep;
int q[N * 2], gap[N * 2];
// vector< vector<Edge> > v;
vector<Edge> v[N * 2];
ISAP(int n, int m, int s, int t) : n(n), m(m), s(s), t(t) {
input();
}
inline void input() {
// v.resize(n + 1);
for (int x, y, f, i(0); i ^ m; ++i) {
read(x, y, f);
v[x].push_back(Edge(y, v[y].size(), f));
v[y].push_back(Edge(x, v[x].size() - 1, 0));
}
}
inline void init() {
dep.assign(n + 1, -1);
dep[t] = 0, gap[0] = 1;
// 如果要用手写队列,要开大一点……避免玄学RE,虽然理论上N就够了
register int qt(0), qf(0);
q[qt++] = t;
int x, y;
while (qf ^ qt) {
x = q[qf++];
for (auto &e : v[x]) {
if (dep[(y = e.to)] == -1) // if dep[y] != -1
++gap[(dep[y] = dep[x] + 1)], q[qt++] = y;
}
} // bfs end
}
inline Data sap(int x, Data flow) {
if (x == t) return flow;
Data rest = flow;
int y, f;
for (auto &e : v[x]) {
if (dep[(y = e.to)] + 1 == dep[x] && e.flow) {
f = sap(y, min(e.flow, rest));
if (f) {
e.flow -= f, v[e.to][e.rev].flow += f;
rest -= f;
}
if (!rest) return flow; // flow all used
}
}
// change dep
if (--gap[dep[x]] == 0) dep[s] = n + 1; // can not reach to t
++gap[++dep[x]]; // ++depth
return flow - rest;
}
inline Data calc() {
Data maxflow(0);
static const Data INF(numeric_limits<Data>::max());
// dep[s]最大为n,为一条链的时候
while (dep[s] <= n) {
// 如果要当前弧优化,在这里需要重置当前弧的now!
maxflow += sap(s, INF);
}
return maxflow;
}
};
int main() {
int n, m, s, t;
read(n, m, s, t);
static ISAP isap(n, m, s, t);
isap.init();
printf("%lld\n", isap.calc());
return 0;
}
?: 如果我想用vector存图实现当前弧优化怎么整
在sap函数的主体部分
for (int & i = now[x]; i < G[x].size(); ++i) {
Edge & e = G[x][i];
if (dep[(y = e.to)] + 1 == dep[x] && e.flow) {
f = sap(y, min(rest, e.flow));
if (f) {
rest -= f, e.flow -= f;
G[e.to][e.rev].flow += f;
}
if (!rest) return flow;
}
}
在calc不部分
while (dep[s] <= n) {
now.assign(n, 0);
maxflow += sap(s, INF);
}
然后……就搞定了QwQ
作者有话说
一般来说,如果图非常稠密(边数远远大于点数),当前弧优化的力度就非常大了
如:Zoj3229 Shoot the Bullet|东方文花帖|【模板】有源汇上下界最大流 - 洛谷
这个专题我会放在网络流的其他部分详解,敬请期待……
写了当前弧优化的Dinic能轻松过……没写全TLE
虽然没写当前弧优化的ISAP能更快的过前三个点,但最后一个点过不了……QwQ
我没有试过当前弧优化的ISAP
更新:有当前弧优化的ISAP可以过
但是如果边数不多,当前弧优化可能就成了负优化了……所以需要根据题目数据合理使用
算法学习笔记(8.1): 网络最大流算法 EK, Dinic, ISAP的更多相关文章
- 图论算法-网络最大流【EK;Dinic】
图论算法-网络最大流模板[EK;Dinic] EK模板 每次找出增广后残量网络中的最小残量增加流量 const int inf=1e9; int n,m,s,t; struct node{int v, ...
- HLPP算法 一种高效的网络最大流算法
#include <algorithm> #include <cstdio> #include <cctype> #include <queue> #d ...
- 「模板」网络最大流 FF && EK && Dinic && SAP && ISAP
话不多说上代码. Ford-Fulkerson(FF) #include <algorithm> #include <climits> #include <cstdio& ...
- 初探网络流:dinic/EK算法学习笔记
前记 这些是初一暑假的事: "都快初二了,连网络流都不会,你好菜啊!!!" from 某机房大佬 to 蒟蒻我. flag:--NOIP后要学网络流 咕咕咕------------ ...
- 【学习笔记】Iperf3网络性能测试工具
[学习笔记]Iperf3网络性能测试工具 网络性能评估主要是监测网络带宽的使用率,将网络带宽利用最大化是保证网络性能的基础,但是由于网络设计不合理.网络存在安全漏洞等原因,都会导致网络带宽利用率不高. ...
- python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例
python3.4学习笔记(十四) 网络爬虫实例代码,抓取新浪爱彩双色球开奖数据实例 新浪爱彩双色球开奖数据URL:http://zst.aicai.com/ssq/openInfo/ 最终输出结果格 ...
- C / C++算法学习笔记(8)-SHELL排序
原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...
- Manacher算法学习笔记 | LeetCode#5
Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...
- Effective STL 学习笔记 Item 34: 了解哪些算法希望输入有序数据
Effective STL 学习笔记 Item 34: 了解哪些算法希望输入有序数据 */--> div.org-src-container { font-size: 85%; font-fam ...
- 【HLSL学习笔记】WPF Shader Effect Library算法解读之[DirectionalBlur]
原文:[HLSL学习笔记]WPF Shader Effect Library算法解读之[DirectionalBlur] 方位模糊是一个按照指定角度循环位移并叠加纹理,最后平均颜色值并输出的一种特效. ...
随机推荐
- 有人相爱,有人夜里开车看海,有人leetcode第一题都做不出来。
第一题 给定一个整数数组 nums 和一个整数目标值 target,请你在该数组中找出 和为目标值 target 的那 两个 整数,并返回它们的数组下标. 你可以假设每种输入只会对应一个答案.但是,数 ...
- 1、使用简单工厂模式设计能够实现包含加法(+)、减法(-)、乘法(*)、除法(/)四种运算的计算机程序,要求输入两个数和运算符,得到运算结果。要求使用相关的工具绘制UML类图并严格按照类图的设计编写程
1.使用简单工厂模式设计能够实现包含加法(+).减法(-).乘法(*).除法(/)四种运算的计算机程序,要求输入两个数和运算符,得到运算结果.要求使用相关的工具绘制UML类图并严格按照类图的设计编写程 ...
- 后端框架的学习----mybatis框架(6、日志)
六.日志 如果一个数据库操作,出现了异常,我们需要排错,日志就是最好的帮手 setting设置 <settings> <setting name="logImpl" ...
- Teambition企业内部应用开发指南
Teambition企业内部应用Python开发指南 注意:此文章并非搬运,一小部分仅为借鉴. Teambition提供了API接口,我们可以注册成为开发者,然后通过接口获取Teambition的数据 ...
- .NET性能系列文章一:.NET7的性能改进
这些方法在.NET7中变得更快 照片来自 CHUTTERSNAP 的 Unsplash 欢迎阅读.NET性能系列的第一章.这一系列的特点是对.NET世界中许多不同的主题进行研究.比较性能.正如标题所说 ...
- Codeforces Round #829 (Div. 2) A-E
比赛链接 A 题解 知识点:枚举. 只要一个Q后面有一个A对应即可,从后往前遍历,记录A的数量,遇到Q则数量减一,如果某次Q计数为0则NO. 时间复杂度 \(O(n)\) 空间复杂度 \(O(1)\) ...
- 九、kubernetes命令行工具kubectl
为了方便在命令行下对集群.节点.pod进行管理,kubernetes官方提供了一个管理命令:kubectl kubectl作为客户端CLI工具,可以让用户通过命令行对Kubernetes集群进行操作. ...
- Java多线程-ThreadPool线程池(三)
开完一趟车完整的过程是启动.行驶和停车,但老司机都知道,真正费油的不是行驶,而是长时间的怠速.频繁地踩刹车等动作.因为在速度切换的过程中,发送机要多做一些工作,当然就要多费一些油. 而一个Java线程 ...
- G1 垃圾收集器深入剖析(图文超详解)
G1(Garbage First)垃圾收集器是目前垃圾回收技术最前沿的成果之一. G1 同 CMS 垃圾回收器一样,关注最小时延的垃圾回收器,适合大尺寸堆内存的垃圾收集.但是,G1 最大的特点是引入分 ...
- golang实现一个简单的http代理
代理是网络中的一项重要的功能,其功能就是代理网络用户去取得网络信息.形象的说:它是网络信息的中转站,对于客户端来说,代理扮演的是服务器的角色,接收请求报文,返回响应报文:对于web服务器来说,代理扮演 ...