P4722 题解
前言
2024.1.11 update :解决被 hack 的问题;增加了更多的补充说明;算法模拟样例过程的图;修改错别字。
HLPP 求网络最大流
先简单说说 HLPP 这个神仙般的算法,时间复杂度达到了惊人的
HLPP 预流推进算法,我们将网络理解成纵横交织的河道,源点是水库,汇点是运输目标,其余的节点就是转折区,有向边是河流。
那么它是如何实现的呢?我们允许网络在非汇点、源点的节点停留(也就是说这是一个一步一步实现的过程,并非像 Dinic 那样一口气从源点走到汇点,而是一种整体最优观念),那么对于每一个节点会拥有一个超额流(也称余流),我们用
需要注意的是,为了推送不陷入死循环,我们引入每个节点的高度
既然引入了
记住一句话,走到死路不要紧,只要每次走的都是最优的路。
还有一点想要提一下,反向连接的边可以起到反悔的作用(让流量流回去)。
接下来,我将重点讲一讲预流推进的具体实现。
定义一个优先队列,以高度为优先,因为我们的推送过程是需要从高到低的(这样才能保证你在处理当前节点的时候不会还有别的节点可以给你推送超额流)。原本每个节点高度为
考虑如何处理优先队列中的节点,我们开始处理优先队列中的队首
然后说一下如何推送,遍历
补充一点,重贴标记如果失败就让他到源点上面去,由于重贴失败了,所以它一定能够流回源点。
最终
花了很长时间才将 luogu 样例的维护过程画了出来,但还是很丑,希望大家见谅,感性理解一下吧,特别是上面文字没完全明白的同学,重点注意薄荷绿五角星位置的流量出入与深紫色五角星节点的高度改变,搞清楚这个图那么这个算法就没什么太大问题。
优化:
可以使用 ISAP 的
非源点高度设为
然后就是我们的
AC Code of Luogu P4722 【模板】最大流 加强版 / 预流推进
/*
Luogu name: Symbolize
Luogu uid: 672793
*/
#include<bits/stdc++.h>
#define int long long
#define pii pair<int,int>
#define x first
#define y second
#define rep1(i,l,r) for(register int i=l;i<=r;++i)
#define rep2(i,l,r) for(register int i=l;i>=r;--i)
#define rep3(i,x,y,z) for(register int i=x[y];~i;i=z[i])
#define rep4(i,x) for(auto i:x)
#define debug() puts("----------")
const int N=1e6+10;
const int inf=0x3f3f3f3f3f3f3f3f;
using namespace std;
int n,m,s,t,e[N],h[N],ne[N],w[N],idx,highh[N],ef[N],gap[N],vis[N];
struct cmp{bool operator()(int a,int b)const{return highh[a]<highh[b];}};//手动定义排序规则
priority_queue<int,vector<int>,cmp> q;
int read()
{
int x=0,f=1;
char ch=getchar();
while(ch<'0'||ch>'9')
{
if(ch=='-') f=-1;
ch=getchar();
}
while(ch>='0'&&ch<='9')
{
x=(x<<3)+(x<<1)+(ch^48);
ch=getchar();
}
return x*f;
}
void add(int x,int y,int z)
{
idx+=2;//建立双向边
e[idx]=y;
ne[idx]=h[x];
w[idx]=z;
h[x]=idx;
e[idx+1]=x;
ne[idx+1]=h[y];
w[idx+1]=0;//反相边的权值为0
h[y]=idx+1;
return;
}
bool bfs()//bfs把初始的高度定义为到t的距离,s则为n
{
queue<int> q;
memset(highh,0x3f,sizeof highh);
highh[t]=0;//t的高度为0,起点
q.push(t);//从t开始
while(!q.empty())
{
int x=q.front();
q.pop();
for(int i=h[x];~i;i=ne[i])
{
int to=e[i];
if(w[i^1]/*剩余容量不为0*/&&highh[to]>highh[x]+1/*可以流向当前目标节点*/)
{
highh[to]=highh[x]+1;//更新高度
q.push(to);//进队
}
}
}
//判断是否连通
if(highh[s]!=inf) return 1;
return 0;
}
void push(int x)
{
rep3(i,h,x,ne)
{
int to=e[i];
if(w[i]/*剩余容量不为0*/&&highh[to]+1==highh[x]/*高度满足*/&&highh[to]<inf)
{
int d=min(ef[x],w[i]);//推送余留不得大于当前节点余流和剩余容量
w[i]-=d;//当前边的容量减掉
w[i^1]+=d;//方向边的流量加上
ef[x]-=d;//当前节点余留减少
ef[to]+=d;//增加目标余流
if(to!=s/*不为源点*/&&to!=t/*不为汇点*/&&!vis[to]/*不在优先队列中*/)
{
q.push(to);//加入优先队列等待推送
vis[to]=1;//以加入优先队列
}
if(!ef[x]) break;//没有余流了
}
}
return;
}
void relabel(int x)//重贴标签
{
highh[x]=inf;
rep3(i,h,x,ne)
{
int to=e[i];
if(w[i]/*剩余容量不为0*/&&highh[to]+1<highh[x]/*保证下一次可以推送即可,不要过高*/) highh[x]=highh[to]+1;
}
return;
}
int HLPP()//核心操作
{
if(!bfs()) return 0;
highh[s]=n;//源点的高度为n
memset(gap,0,sizeof gap);
rep1(i,1,n) if(highh[i]<inf/*没法到达的点就不用了*/) ++gap[highh[i]];//gap优化
//为了防止发生一些问题,单独跑一边源点
rep3(i,h,s,ne)
{
int to=e[i];
int dist=w[i];
if(dist&&highh[to]<inf)
{
w[i]-=dist;//当前边容量减少
w[i^1]+=dist;//反相边流量增加
ef[s]-=dist;//当前节点余流推送
ef[to]+=dist;//被推送节点余流增加
if(to!=s/*不为源点*/&&to!=t/*不为汇点*/&&!vis[to]/*不在优先队列中*/)
{
q.push(to);//等待推送
vis[to]=1;//已进入优先队列
}
}
}
while(!q.empty())
{
int x=q.top();//当前推送节点
vis[x]=0;//取消标记
q.pop();//出队
push(x);//推送
if(ef[x])//还有余流
{
if(!--gap[highh[x]]/*如果本高度只有它一个*/) rep1(j,1,n) if(j!=s/*不为源点*/&&j!=t/*不为汇点*/&&highh[j]>highh[x]/*不可能被推送到汇点*/&&highh[j]<n+1/*高度不等于n+1*/) highh[j]=n+1;//把不可能到达汇点的所有点高度调为n+1,直接流向源点
relabel(x);//重贴标签,保证下次能够推送
++gap[highh[x]];//新的高度
q.push(x);//因为还有余流所以进队
vis[x]=1;//标记
}
}
return ef[t];//返回汇点的余流,这就是网络最大流
}
signed main()
{
memset(h,-1,sizeof h);
n=read();
m=read();
s=read();
t=read();
rep1(i,1,m)
{
int u=read();
int v=read();
int w=read();
add(u,v,w);//连边
}
cout<<HLPP()<<endl;
return 0;
}