网络流——最大流——HLPP学习笔记
前言
在刚了解最大流算法时,看到 Ford-Fulkerson(基于 DFS 求解增广路)(
所以,在学完 Edmonds-Karp 后,我认为应该学一些性能比较好的最大流算法(绝对不是因为我看不懂 Dinic)。于是,在 Oi-wiki 的最大流页面的最后,我找到了一个被普遍认为是复杂度最优的最大流算法——HLPP。
简介
不同于前面列举的三个算法,HLPP 求解最大流基于一种叫“预留推进算法”的东西。
超额流
预留推进算法不要求在求解过程中,流量始终保持守恒。它规定了某个节点的超额流
若
高度函数
同时,预留推进算法规定结点的
推送(Push)
若结点
则
重贴标签(Relabel)
如果当前结点
实现
那么,如何实现 HLPP 算法呢?
初始化除源点外所有结点高度为
首先推送
接着,选出所有溢出结点中高度最小的那个
这样不停循环直到除源点、汇点外所有点都已不再溢出。此时
但是,这样写的话并不能达到最优复杂度,会跑得很慢。所以,我们要使用两个优化:BFS 优化和 GAP 优化。
BFS 优化
开头在反图上从
GAP 优化
因为推送条件是
具体地,我们可以通过一个数组、一个桶来实现 GAP 优化。详见代码。
代码
#include <bits/stdc++.h>
#define ll long long
using namespace std;
const int maxn = 205, maxm = 5e3 + 5, INF = 114514;
int n, m, s, t;
int head[maxn], nxt[maxm << 1], to[maxm << 1], cnt = 1;
ll flow[maxm << 1];
int hi[maxn];
ll ex[maxn], gap[maxn]; //hi:结点高度。ex:溢出流量。gap:高度为 i 的结点数量。
stack<int> B[maxn]; //B:存储高度为 i 的 溢出 结点。
int lev = 0; //lev:溢出 结点的最大高度。
void merge(int u, int v, int w){
nxt[++ cnt] = head[u];
to[cnt] = v;
flow[cnt] = w;
head[u] = cnt;
}
void merge_flow(int u, int v, int w){
merge(u, v, w);
merge(v, u, 0);
}
bool push(int x){ //从结点 x 推送流。如果没推送完返回 true。
bool init = x == s; //标记是否正在初始化。
for(int i = head[x]; i; i = nxt[i]){
int j = to[i];
if(!flow[i] || (!init && hi[x] != hi[j] + 1) || hi[j] == INF) continue; //该边流量为 0、未在初始化时当前点高度不是目标点加一、目标点高度无穷大都不能推送(注意初始化时不考虑高度差为 1)。
ll nw = init ? flow[i] : min(ex[x], flow[i]); //nw:可推送流量(注意初始化时不考虑源点溢出流量)。
if(j != s && j != t && !ex[j]) B[hi[j]].push(j), lev = max(lev, hi[j]); //若 j 点溢出,将其记录。
ex[x] -= nw, ex[j] += nw, flow[i] -= nw, flow[i ^ 1] += nw; //更新流量。
if(!ex[x]) return false;
}
return true;
}
void relabel(int x){ //重贴标签(即重置结点 x 的高度)。
hi[x] = INF; //先重置为无穷大。
for(int i = head[x]; i; i = nxt[i]){
int j = to[i];
if(flow[i]) hi[x] = min(hi[x], hi[j]); //在结点 x 能到达的结点中找到高度最小的。
}
hi[x] ++; //高度要加 1。
if(hi[x] < n){ //只处理高度小于 n 的结点。
B[hi[x]].push(x); //重新记录 x。
lev = max(lev, hi[x]);
gap[hi[x]] ++;
}
}
bool bfs_init(){ //bfs 初始化高度。
for(int i = 1; i <= n; i ++) hi[i] = INF;
queue<int> q;
q.push(t); //从汇点 t 开始跑 bfs。
hi[t] = 0; //汇点 t 高度为 0。
while(!q.empty()){
int x = q.front();
q.pop();
for(int i = head[x]; i; i = nxt[i]){
int j = to[i];
if(flow[i ^ 1] && hi[x] + 1 < hi[j]){
hi[j] = hi[x] + 1;
q.push(j);
}
}
}
return hi[s] != INF; //如果不连通则返回 false。
}
int select(){ //选择高度最大的溢出结点。如果没有溢出结点说明程序结束,返回 0。
while(B[lev].empty() && lev >= 0) lev --;
return lev == -1 ? 0 : B[lev].top();
}
ll HLPP(){
if(!bfs_init()) return 0; //若图不连通,直接返回 0。
for(int i = 1; i <= n; i ++) gap[i] = 0;
for(int i = 1; i <= n; i ++) if(hi[i] != INF) gap[hi[i]] ++; //初始化 gap。
hi[s] = n; //源点 s 的高度定为 n。
push(s); //初始化预流。
int x = 0;
while(x = select()){ //不断选出高度最大的溢出结点。
B[lev].pop(); //选出来的结点,要么不再溢出,要么需要重贴标签,故直接弹出。
if(push(x)){ //若推送后仍溢出。
gap[hi[x]] --; //x 的高度需要修改。
if(!gap[hi[x]]){ //如果高度为 x 的点不存在了。
for(int i = 1; i <= n; i ++){
if(i != s && hi[i] > hi[x] && hi[i] < n + 1) hi[i] = n + 1; //根据 GAP 优化,当前高度为 hi[x] 的结点不存在了,所以所有高度大于 hi[x] 的结点不可能将溢出流推送至 t。
}
}
relabel(x); //此时需要重贴标签。
}
}
return ex[t]; //答案即为最后 t 点的溢出流量。
}
int main(){
scanf("%d %d %d %d", &n, &m, &s, &t);
for(int i = 1; i <= m; i ++){
int u, v, c;
scanf("%d %d %d", &u, &v, &c);
merge_flow(u, v, c);
}
printf("%lld", HLPP());
return 0;
}
易错点
-
记得初始化
h(s)=n 。 -
记得特判一些源点、汇点不适用的操作。
-
记得在结点高度改变时一定要考虑
gap,B,lev 的变化。