網絡最大流——最高標號預流推進


0、寫在前面

解決最大流問題,最常用的算法是\(Dinic\)\(ISAP\),兩種算法在實際應用中都相當快速。但是就理論而言,兩種算法都屬於增廣路算法,復雜度上界都達到了\(O(n^2 m)\),因此在某些題目中或許存在毒瘤出題人構造數據卡這兩種算法的情況。因此本文講解一種復雜度上界在常用最大流算法中最優的最高標號預留推進算法(又叫\(HLPP\)),其上界為\(O(n^2 \sqrt m)\)並且在經過優化后這種算法在數據隨機的情況下速度也不亞於上述兩種增廣路算法

抱歉...我已經 絕對不可能再獲得幸福了

1、預流推進的思想

\(HLPP\)是預流推進算法中的一種,介紹一下預留推進算法的思想:

我們還是以水流來類比網絡流。我們將每個點看作一個水庫,每條邊看作單向的管道。預流推進算法的思想就是,允許水在非源匯點的節點中暫時存儲(我們將存儲在非源匯點中的流稱作這個點的超額流),同時伺機將自身的超額流通過管道推送出去。只要保證在算法結束后所有非源匯點的超額流都為\(0\),那么這種方案就是合法的。

同時為了避免反復推送而出現死循環的問題,我們再給每個節點引入高度的概念。同實際中一樣,我們只允許高度較高的節點向高度較低的節點進行推送(不過還是略有不同,具體請見下文)。如果一個節點因為受到高度的限制而不能推送自身的超額流,那么我們就抬高這個節點的高度,我們把這個操作叫做重貼標簽

因為...我發現...

2、最高標號預流推進的過程

下面介紹\(HLPP\)的具體流程,在下文中,我們用\(e_v\)表示節點\(v\)的超額流,用\(h_v\)表示節點\(v\)的高度。

首先,我們將除了源點以外的節點的高度都置為\(0\)。特別地,我們將源點\(s\)的高度置為\(n\),並且將\(s\)的所有邊都充滿流量推送出去。我們不必擔心這種操作會讓流變得太多,因為多余的流量會在別的節點高度抬高到超過\(s\)以后推送回去。我們還應該將推送過后超額流不為\(0\)的節點加入隊列中等待推送,同時,因為\(HLPP\)會要求我們每次都推送高度最高的節點(注意是對於超額流不為\(0\)的節點而言的),因此這應當是一個以高度為鍵值的優先隊列。

在進行完上述操作以后,我們開始處理優先隊列。我們每次都取出隊首\(v\)並嘗試推送,推送過程如下:

逐一檢查\(v\)的出邊,如果發現某條邊還有流量並且邊的另一端\(u\)滿足\(h_u+1=h_v\),即\(v\)正好比\(u\)的高度高\(1\)(注意在初始操作中對\(s\)的推送並沒有這個要求),那么這條邊就是可以推送的。我們推送的流量應當既不可以超過這條邊的流量,也不可以超過\(e_v\)。在推送完成后記得修改\(u\)\(v\)的超額流,並且如果\(u\)還不在優先隊列中,那么應當將\(u\)加入優先隊列。

推送過程執行完畢后,如果我們發現\(e_v\)仍然不等於\(0\),說明當前的高度\(h_v\)並不夠,因此我們對\(v\)重貼標簽。我們找到有流量而且邊的另一端\(u\)高度最小的邊,將\(h_v\)設置為\(h_u+1\),這樣就保證了下次一定可以推送。於是我們再將重貼標簽后的\(v\)加入優先隊列中。

如果優先隊列為空,那么除了源匯點以外的所有節點超額流都為\(0\),這是一個合法的流,並且也是最大流。

其實我...早就已經被幸福包圍了

3、一點優化

雖然上述的算法是正確的,並且復雜度是\(O(n^2 \sqrt m)\)的,但是因為缺少一些優化,使得其上界比較緊,因此在隨機數據下可能不如增廣路算法。下面介紹對\(HLPP\)的優化,使得其在隨機數據下的速度可以與增廣路算法媲美。

首先,我們可以發現將所有非源點的高度設置為\(0\)是有些浪費的。我們不妨通過一遍\(bfs\)將每個點的初始高度設置為它到匯點\(t\)的最短距離,這樣就節省了大量重貼標簽操作。當然,源點\(s\)的高度還是應該設置為\(n\)

然后,我們還可以發現如果一個點\(v\)再被重貼標簽以后,如果它原來的高度已經沒有其它點,那么高於它的點一定不能將流量推送到\(t\)了。所以我們可以將高度大於\(h_v\)且小於\(n+1\)的點高度設置為\(n+1\),以便盡快將流量推送給\(s\)。對於如何判斷這個高度已經沒有其它節點,可以和\(ISAP\)一樣用一個\(gap\)數組來計數,這就是\(HLPP\)\(gap\)優化。

——珂朵莉·諾塔·瑟尼歐里斯

4、C++實現

因為\(HLPP\)的細節還是挺多的,這里就放一下我的代碼吧。\(HLPP\)的碼量其實不多,我這樣的寫法也只是\(100\)行出頭而已。所以在網絡流問題中使用\(HLPP\)也是一個不錯的選擇。

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<queue>
using std::min;
using std::vector;
using std::queue;
using std::priority_queue;
const int N=2e4+5,M=2e5+5,inf=0x3f3f3f3f;
int n,s,t,tot;
int v[M<<1],w[M<<1],first[N],next[M<<1];
int h[N],e[N],gap[N<<1],inq[N];//節點高度是可以到達2n-1的
struct cmp
{
	inline bool operator()(int a,int b) const
	{
		return h[a]<h[b];//因為在優先隊列中的節點高度不會改變,所以可以直接比較
	}
};
queue<int> Q;
priority_queue<int,vector<int>,cmp> pQ;
inline void add_edge(int from,int to,int flow)
{
	tot+=2;
	v[tot+1]=from;v[tot]=to;w[tot]=flow;w[tot+1]=0;
	next[tot]=first[from];first[from]=tot;
	next[tot+1]=first[to];first[to]=tot+1;
	return;
}
inline bool bfs()
{
	int now;
	register int go;
	memset(h+1,0x3f,sizeof(int)*n);
	h[t]=0;Q.push(t);
	while(!Q.empty())
	{
		now=Q.front();Q.pop();
		for(go=first[now];go;go=next[go])
			if(w[go^1]&&h[v[go]]>h[now]+1)
				h[v[go]]=h[now]+1,Q.push(v[go]);
	}
	return h[s]!=inf;
}
inline void push(int now)//推送
{
	int d;
	register int go;
	for(go=first[now];go;go=next[go])
		if(w[go]&&h[v[go]]+1==h[now])
		{
			d=min(e[now],w[go]);
			w[go]-=d;w[go^1]+=d;e[now]-=d;e[v[go]]+=d;
			if(v[go]!=s&&v[go]!=t&&!inq[v[go]])
				pQ.push(v[go]),inq[v[go]]=1;
			if(!e[now])//已經推送完畢可以直接退出
				break;
		}
	return;
}
inline void relabel(int now)//重貼標簽
{
	register int go;
	h[now]=inf;
	for(go=first[now];go;go=next[go])
		if(w[go]&&h[v[go]]+1<h[now])
			h[now]=h[v[go]]+1;
	return;
}
inline int hlpp()
{
	int now,d;
	register int i,go;
	if(!bfs())//s和t不連通
		return 0;
	h[s]=n;
	memset(gap,0,sizeof(int)*(n<<1));
	for(i=1;i<=n;i++)
		if(h[i]<inf)
			++gap[h[i]];
	for(go=first[s];go;go=next[go])
		if(d=w[go])
		{
			w[go]-=d;w[go^1]+=d;e[s]-=d;e[v[go]]+=d;
			if(v[go]!=s&&v[go]!=t&&!inq[v[go]])
				pQ.push(v[go]),inq[v[go]]=1;
		}
	while(!pQ.empty())
	{
		inq[now=pQ.top()]=0;pQ.pop();push(now);
		if(e[now])
		{
			if(!--gap[h[now]])//gap優化,因為當前節點是最高的所以修改的節點一定不在優先隊列中,不必擔心修改對優先隊列會造成影響
				for(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]];
			pQ.push(now);inq[now]=1;
		}
	}
	return e[t];
}
int m;
signed main()
{
	int u,v,w;
	scanf("%d%d%d%d",&n,&m,&s,&t);
	while(m--)
	{
		scanf("%d%d%d",&u,&v,&w);
		add_edge(u,v,w);
	}
	printf("%d\n",hlpp());
	return 0;
}


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM