关于 HLPP
Legitimity · · 题解
来一个 HLPP 的题解
HLPP (最高标号预流推进)应该是常见最大流算法里效率最高的,最差复杂度为
基本思想
EK、FF、dinic、ISAP 等算法都属于增广路算法。而 HLPP 是一种推送-重贴标签算法(又称预流推进)。现在效率最高的几种最大流算法几乎都是推送-重贴标签算法。推送-重贴标签算法在算法过程中并不满足网络的流量守恒性质,即存在流入一个节点的流量大于流出该节点流量的情况,因为在执行过程中,推送-重贴标签算法始终维护着一个函数预流(preflow)。
众所周知,网络的流量守恒性质是:在网络
而在推送-重贴标签算法过程中,则弱化了这个条件:
即每个节点可以暂时储存一定的流量(但不能透支),而我们称:
为
不难发现,在最大化流量且
那么我们的目标就是不断推送流量,且保证最终
算法流程
大家可能就立刻会想到一种做法:
每个节点都把流向自己的边流满,多余的流量作为超额流暂时储存,一旦有机会就马上推向周围的节点;因为有反向边的存在,从
但是,设想一种情况:如果
那么我们就考虑和 dinic 差不多,将整张网络进行分层,每一层节点的超额流只能推给下一层节点。
令
但是,这样又会有另一种情况:在流量推来推去的过程中,有个节点
怎么办?
我们的算法叫推送-重贴标签算法。
那就强行把她抬起来,把流量推回去。
(你可以想象一个场面,一个盆地或一个断层被顶出,变成一座山)
每次遇到流不动的情况,那么就找到一个最低的高度,让她恰好能流掉自己的超额流。
这就是重贴标签(relabel)的过程
然后呢?没有然后了。
关于 HLPP 的细节
上面的都是推送-重贴标签算法的基础模型,如果不加优化可以轻松的达到 优秀复杂度,距离我们 HLPP 的
那么还要注意什么?
- 关于
h 函数初值的处理。
如何给
但这样不够优秀,在初始时可以像 dinic 一样 bfs 一遍,将分层图的层次作为
- 关于怎么让流量从高处向低处流?
这个问题由 R·E·Tarjan (怎么又是他)在 1986 年解决,这也是 HLPP 名字的由来。bfs 可以有层次的处理信息。我们可以建立一个以
- 关于重贴标签的 gap 优化。
学过 ISAP 的人大概都知道 gap 优化,HLPP 的 gap 优化和 ISAP 有点像。假设我们给
代码(细节比较多,可以对照查看):
#include<bits/stdc++.h>
using namespace std;
#define rg register
#define ll long long
#define ull unsigned ll
#define inf 0x7f7f7f7f
#define sit set<int>::iterator
inline void file(){
freopen("test.in","r",stdin);
}
char buf[1<<21],*p1=buf,*p2=buf;
inline int getc(){
return p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<21,stdin),p1==p2)?EOF:*p1++;
}
inline ll read(){
rg ll ret=0,f=0;char ch=getc();
while(!isdigit(ch)){if(ch=='-')f=1;ch=getc();}
while(isdigit(ch)){ret=ret*10+ch-48;ch=getc();}
return f?-ret:ret;
}
struct node{
int nex,to;
ll val;
}e[500005<<1];
int n,m,u,v,w,s,t,cnt=1,head[50005];
int h[50005],gap[50005]; //h 表示高度,gap 表示该高度的节点数。
bool vis[50005]; //vis 表示是否在队中。
ll maxflow,excess[50005]; //excess 表示超额流
inline void add(int u,int v,ll w){
e[++cnt].nex=head[u];
e[cnt].to=v;
e[cnt].val=w;
head[u]=cnt;
}
bool init(){ //初始 bfs 给图分层,注意是从 t 倒着搜。
queue<int> q;
memset(h,0x7f,sizeof(h));
h[t]=0; q.push(t);
while(!q.empty()){
int now=q.front(); q.pop();
for(rg int i=head[now];i;i=e[i].nex){
if(e[i^1].val&&h[e[i].to]-1>h[now]){
q.push(e[i].to);
h[e[i].to]=h[now]+1;
}
}
}
return h[s]!=inf;
}
struct ty{
int v;
bool operator<(const ty& __)const{
return h[v]<h[__.v];
}
};
inline ty make(int x){
return (ty){x};
}
priority_queue<ty> pq; //以 h 为键值的优先队列。
inline void push(int x){ //预流推进操作。
for(rg int i=head[x];i;i=e[i].nex){
if(!excess[x]) break; //推完了。
if(!e[i].val||h[e[i].to]!=h[x]-1) continue; //不能推。
ll tmp=min(e[i].val,excess[x]); //尽可能的推。
e[i].val-=tmp;
e[i^1].val+=tmp;
excess[x]-=tmp;
excess[e[i].to]+=tmp;
if(e[i].to!=s&&e[i].to!=t&&!vis[e[i].to]){ //被推的节点自己也会超额,入队等待推别人的机会。
vis[e[i].to]=1;
pq.push(make(e[i].to));
}
}
}
inline void relabel(int x){
h[x]=inf;
for(rg int i=head[x];i;i=e[i].nex)
if(e[i].val&&h[e[i].to]<h[x]-1)
h[x]=h[e[i].to]+1; //找到最矮的能推别人的位置。
}
void HLPP(){
if(!init()) //不连通。
return maxflow=0,void();
h[s]=n;
for(rg int i=1;i<=n;++i)
if(h[i]!=inf) ++gap[h[i]];
for(rg int i=head[s];i;i=e[i].nex){
if(!e[i].val||h[e[i].to]==inf) continue;
ll tmp=e[i].val;
e[i].val-=tmp;
e[i^1].val+=tmp;
excess[s]-=tmp;
excess[e[i].to]+=tmp;
if(e[i].to!=s&&e[i].to!=t&&!vis[e[i].to]){
vis[e[i].to]=1;
pq.push(make(e[i].to));
}
} //同 push 函数,对于 s 的 push 特殊处理。
while(!pq.empty()){
int now=pq.top().v; pq.pop();
vis[now]=0;
push(now);
if(!excess[now]) continue; //超额流推完了。
if(!--gap[h[now]]) //gap 优化。
for(rg int i=1;i<=n;++i)
if(i!=s&&i!=t&&h[i]>h[now]&&h[i]<n+1)
h[i]=n+1;
relabel(now);
++gap[h[now]]; //重贴标签。
vis[now]=1;
pq.push(make(now)); //没推完,入队继续等待推别人的机会。
}
maxflow=excess[t];
}
int main(){
n=read(); m=read(); s=read(); t=read();
for(rg int i=1;i<=m;++i){
u=read(); v=read(); w=read();
add(u,v,w);
add(v,u,0);
}
HLPP();
printf("%lld",maxflow);
return 0;
}
//If you're gonna replace me,at least have the audacity to kill me thoroughly.