HLPP 预流推进

Decribe:

给定 n 个点 m 条边,每条边有一个流量 f。给定起点 s 和终点 t,求最大流。(n1200,m120000

Solution:

n,m 来到这样一个上界,Dinic 稍稍被卡就过不去了,与其研究奇奇怪怪的 Dinic 优化,真不如学一个 HLPP,它又不难。(又不会吃掉你)

学过 ISAP 会更好学 HLPP,它们之间有许多的共性。

我们先回忆一下 Dinic 的运行过程:先找到一条或几条增广路,推送流量,再重新找增广路,推送流量,直至找不到增广路为止。这样的算法因为 spfa 的原因,可以卡到 O(n2m),(如果没有当前弧优化的话,那将是 O(nm2))这是难以接受的。每次寻找增广路时,都有大量的时间浪费在无用的路径。因此就有了 ISAP,它就在推送流量的 dfs 中,尝试更新增广路,因此往往也更快。(但时间复杂度实际是相同的)

推流方案

但这还不够快。于是 HLPP 出现了,时间复杂度上界仅有 O(n2m)。(没用当前弧优化的话,当然是 O(nmm))让我们回归最初的想法。一开始接触到最大流时,应该有很多人的想法是让流量同时从起点全部流出,看能到达多少。HLPP 基本遵循了这个想法。首先先从起点无视流量守恒,全部推流出去,每个被推流的点获得一个余流。再遍历每个有余流的点,将余流推流。但是流向何处呢?若不加以限制,势必会出现两个点和一条边打乒乓球,你流过来,我流过去,获得一个完美的 TLE。

确定流向

想想增广路是怎么确定路线的?通过每一次的 spfa,对每个点标记分层图高度,规定流量只能从上往下流。而 ISAP 中增广一次后,让每个推流过的点向上爬,尝试找到新的增广路。于是我们也可以给定每个点一个高度,规定流量只能从上往下一层一层的流,就像水往低处流一样。如果已经没有合适的路径了,就尝试遍历每条仍有流量的边对应的点,更新为 最低点+1,这样可以保证有新的一条路可以推流,也不会漏掉某些还未推流的边。相比于 ISAP 的一层层爬,效率可谓是天差地别。

减少推流次数

为减少推流次数,显然从高到低的顺序推流可以最大程度减少推流的次数,因此可以用一个以高度为排序键值的大根堆优先队列存点,每次取队头推流。

一些小细节

最终的网络还是要遵循流量守恒的。也就是说,多余的流量最终会回到起点。所以起点的高度要定为 n,这样既不会一开始就流回来,也不会增加太多的推流次数。

起点和终点是不能入队的。起点有无限余流,终点没有余流。

To be better

但这样只是最基础的 HLPP,非常容易达到上界,甚至在随机图的效率远不如 Dinic。我们需要加亿一点点的优化,以食用更好的 HLPP。

gap 优化

既然类比于 ISAP 的爬层,那为什么不能把 ISAP 的 gap 优化拿来用?若某一高度已经没有点了,即断层,那显然高度更高的点已经无法推流向终点了。若是只需要终点的信息,则可以无视更高的点了。若需要整个图的信息,那就得让更高的点流回起点返还,直接全部设为 n+1 即可。

如果使用 gap 优化(应该没有人不用吧ˋ( ° ▽、° ) ),还有个小细节需要注意:在入队时可能会有高度为 inf 的点,会导致 RE,应在入队时排除。

vector 是个好东西

一般要卡掉 Dinic 而用 HLPP 时,网络往往很稠密,不连续访存的邻接表会有很大的常数。这时可以改用 vector 存图,连续的访存拥有极小的常数。

邻接表:4.73s

vector:1.08s

好用极了!d=====( ̄▽ ̄*)b

bfs 初始化

在一开始设置高度的时候,要从 0 慢慢往上爬,显然浪费了太多的时间。所以可以先从终点 bfs 一遍设置高度,从第一次推流就可以找到路,少了许多进队出队重编号的时间。

叠完 buff 之后,现在的 HLPP 即使在随机图上也不逊色于 Dinic 和 ISAP 了,可供诸君任意使用。

Code:

以下代码加了所有以上优化。

bool _Start;// by Lofty
#include<queue>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
namespace IO
{
	#define TP template<typename T>
	#define TP_ template<typename T,typename ... T_>
	#ifdef DEBUG
	#define gc() (getchar())
	#else
	char buf[1<<20],*p1,*p2;
	#define gc() (p1==p2&&(p2=(p1=buf)+fread(buf,1,1<<20,stdin),p1==p2)?EOF:*p1++)
	#endif
	#ifdef DEBUG
	void pc(const char &c)
	{
		putchar(c);
	}
	#else
	char pbuf[1<<20],*pp=pbuf;
	void pc(const char &c)
	{
		if(pp-pbuf==1<<20)
			fwrite(pbuf,1,1<<20,stdout),pp=pbuf;
		*pp++=c;
	}
	struct IO{~IO(){fwrite(pbuf,1,pp-pbuf,stdout);}}_;
	#endif
	TP void read(T &x)
	{
		x=0;static int f;f=0;static char ch;ch=gc();
		for(;ch<'0'||ch>'9';ch=gc())ch=='-'&&(f=1);
		for(;ch>='0'&&ch<='9';ch=gc())x=(x<<1)+(x<<3)+(ch^48);
		f&&(x=-x);
	}
	TP void write(T x)
	{
		if(x<0)
			pc('-'),x=-x;
		static T sta[35],top;top=0;
		do
			sta[++top]=x%10,x/=10;
		while(x);
		while(top)
			pc(sta[top--]^48);
	}
	TP_ void read(T &x,T_&...y){read(x);read(y...);}
	TP void writeln(const T x){write(x);pc('\n');}
	TP void writesp(const T x){write(x);pc(' ');}
	TP_ void writeln(const T x,const T_ ...y){writesp(x);writeln(y...);}
	TP void debugsp(const T x){fprintf(stderr,"%d ",x);}
	TP void debug(const T x){fprintf(stderr,"%d\n",x);}
	TP_ void debug(const T x,const T_...y){debugsp(x);debug(y...);}
	TP inline T max(const T &a,const T &b){return a>b?a:b;}
	TP_ inline T max(const T &a,const T_&...b){return max(a,max(b...));} 
	TP inline T min(const T &a,const T &b){return a<b?a:b;}
	TP_ inline T min(const T &a,const T_&...b){return min(a,min(b...));}
	TP inline void swap(T &a,T &b){static T t;t=a;a=b;b=t;}
	TP inline T abs(const T &a){return a>0?a:-a;}
	#undef TP
	#undef TP_
}
using namespace IO;
using std::cerr;
using PII=std::pair<int,int>;
using LL=long long;
constexpr int N=5e3+10,M=3e5+10,inf=0x3f3f3f3f;
int n,m,st,ed;
LL w[N];
int h[N],gap[N<<1],cur[N];
struct edge
{
	int y;LL f;int other;
};
std::vector<edge>a[N];
inline void ins(int x,int y,LL f)
{
	a[x].push_back({y,f,(int)a[y].size()});
	a[y].push_back({x,0,(int)a[x].size()-1});
}
struct cmp
{
	inline bool operator()(const int &x,const int &y)const
	{
		return h[x]<h[y];//队内高度不会改变,必须先出队再改,优先队列只在插入和出队时排序
	}
};
bool bfs()//给所有点一个初始的高度,选择最短路的原因是推流的点最少
{
	static std::queue<int>q;
	memset(h,0x3f,sizeof(h));
	h[ed]=1;
	q.push(ed);
	while(q.size())
	{
		int x=q.front();q.pop();
		for(auto k:a[x])
		{
			int y=k.y;
			if(a[y][k.other].f&&h[y]>h[x]+1)
			{
				h[y]=h[x]+1;
				q.push(y);
			}
		}
	}
	return h[st]!=h[0];
}
bool inq[N];
std::priority_queue<int,std::vector<int>,cmp>q;//按高度排序的大根堆
inline void push(int x)//对 x 点的余流推流出去
{
	for(int &i=cur[x];i<(int)a[x].size();i++)//当前弧优化,若保留了当前弧,说明目前余流不足,还可以需要再推流
	{
		edge &k=a[x][i];
		int y=k.y;
		if(k.f&&h[y]==h[x]-1)//可以流且是一条合法的推流路径
		{
			LL f=min(w[x],k.f);
			w[x]-=f;w[y]+=f;
			k.f-=f;
			a[y][k.other].f+=f;
			if(y!=st&&y!=ed&&!inq[y])//加入队列。注意起点和终点不能入队,因为起点有无限余流,终点只接收,没有余流
				q.push(y),inq[y]=1;
			if(!w[x])//没有余流可推了,自然要提前结束
				return ;//这里必须是 return 而不是 break!否则当仍有边可推流时当前弧重置,优化就是假的!(@WisNourx_ 就是这个**写错)
		}
	}
	cur[x]=0;//如果还有余流,就再重新遍历,对应下面改变高度,流向新的路径
}
inline void relabel(int x)//重新给一个高度
{
	h[x]=inf;
	for(auto k:a[x])
		if(k.f&&h[k.y]+1<h[x])//往尽量低处流,因为最低点是终点
			h[x]=h[k.y]+1;
}
LL HLPP()
{
	if(!bfs())//提前给定高度,加速推流,否则需要再推流时给定,增加遍历数量
		return 0;
	for(int i=1;i<=n;i++)
		if(h[i]<inf)
			++gap[h[i]];//gap 优化,若一个高度已经全部流完,更高的是找不到低处流的
						//并且 gap 需要开到 2n-1,最极端的情况可能会全部流回起点
	h[st]=n;//若其他点流完找不到低处,就要流回起点方向,为防可能流向其他方向,应定为 n,这样其他点在初始情况下是不可能高于起点的
	for(auto &k:a[st])//在起点做一次无限流量的推流
	{
		int y=k.y;
		if(k.f&&h[y]<inf)
		{
			LL f=k.f;
			w[st]-=f;w[y]+=f;
			k.f-=f;
			a[y][k.other].f+=f;
			if(y!=st&&y!=ed&&!inq[y])//加入队列。注意起点和终点不能入队,因为起点有无限余流,终点只接收,没有余流
				q.push(y),inq[y]=1;
		}
	}
	while(q.size())
	{
		int x=q.top();q.pop();inq[x]=0;
		push(x);
		if(w[x])
		{
			if(--gap[h[x]]<=0)//没有该高度了
				for(int i=1;i<=n;i++)
					if(i!=st&&i!=ed&&h[i]>h[x]&&h[i]<n+1)//那么比它更高的就没有低处流了,流回起点
						h[i]=n+1;
			relabel(x);++gap[h[x]];
			q.push(x);inq[x]=1;
		}
	}
	return w[ed];
}
bool _End;
int main()
{
//	fprintf(stderr,"%.2 MBlf\n",(&_End-&_Start)/1048576.0);
	read(n,m,st,ed);
	for(int i=1,x,y,f;i<=m;i++)
	{
		read(x,y,f);
		ins(x,y,f);
	}
	writeln(HLPP());
	return 0;
}

posted @   wmtl_lofty  阅读(42)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示