簡單講講上下界網路流

maple276發表於2024-10-04

無源匯可行流

無源匯網路流一般不討論最大流,因為它的流都是環流,分佈在各個位置,一是不好統計,二是一般也沒有意義。所以一般建圖只需要求是否有可行解(但我也沒遇到過求輸出YES和NO的可行流題目,網上的部落格也都只當做有源匯的前置知識講了)

廢話不多說,直接上圖。

sxj1.png

第一張圖給出了無源匯模型每條邊的上下界,綠色數值是每條邊的下界值,即至少要流的流量。假設我們先滿足下界,如果此時各個點綠色流量的流出和流入值相同,那麼直接就是一個可行解,但是顯然大部分情況是無法用下界來滿足平衡的。我們把每條邊上界減去下界的差值當做額外流量,這張圖需要流一些額外流量,才能彌補之前的不平衡。

對於下界流出量大於流入量的點,之前其實沒有足夠的流量供它流出,所以它就需要從前面獲得一些額外流量,呈現缺失態(上圖點 1 和點 4);對於流入量大於流出量的點,它需要透過出邊的額外流量來分掉它的流入量,呈現飽滿態(上圖點 2,3,5)。那麼問題就變成了,能不能找到一條路線,飽滿態的起點把流入它的多餘流量透過這條路線轉移出去,終點獲得它之前缺失的流量,中間的點因為流入又流出,所以出入之差不變,這條路線的意義就是平衡了起點和終點。(路線這個概念很重要,是理解整個上下界網路流的關鍵,它是源匯點任意時的增廣路)

我們要彌補所有點的不平衡,就要找到多條路線。具體的,先把原圖兩點間額外流量設成新圖流量,再設定一對虛擬的源匯點,讓源點向飽滿態的點連一條多餘流量(流入量 - 流出量)的邊,讓缺失態的點向匯點連一條需求流量(流出量-流入量)的邊,跑從源點到匯點的最大流,如果最大流等於多餘流量(或需求流量)之和,那麼就是一組可行解。

初學時有一個疑問就是,飽滿態的點流入量已經大於流出量了,為什麼還要從源點向他連一條流入邊。其實這條虛擬的流入邊不是為了增加這個點的流量,而是將所有飽滿態的點連在一起,方便同時找到多條增廣路,新建的虛擬邊流量在原圖中是沒有任何意義的。

sxj2.png

我們直接來看跑完最大流之後的情況,右圖展示了新圖流到匯點的兩條路徑,紅色部分是找到了兩條路線:2->4->5->1 和 5->1 各自流一個流量,藍色部分找到了一條路線:3->4 流一個流量。在原圖中的體現就是:點 2 透過原圖中三條紅色邊的額外流量,把自己多出來的流量流回了點1 ;點 3 透過原圖中藍色邊的額外流量,把自己多出來的流量流給了點 4。至此,原圖的所有點流入量和流出量平衡,我們找到了一組可行流。想要輸出方案也很簡單,新圖中每條邊的流量是它原圖的額外流量,加上下界,就是原圖的真實流量。

有源匯上下界可行流

其實我們思考下本質,有源匯與無源匯最大的區別,除了長的樣子不同,其實就是我們不再需要平衡源點和匯點的流量,雖然其他點要平衡,但源點可以是一個缺失點,只流出不流入;匯點是一個飽和點,只流入不流出。所以有源匯上下界的一個可行流,需要滿足:除源點和匯點以外,其他點流入量等於流出量

我們可以把它改造成無源匯,即從匯點向源點連一條下界為0,上界無窮的新邊。不過很多部落格講有源匯上下界時沒有具體講這條邊的作用,導致讀者到後面理解最大流和最小流時非常困難。

在沒有這條邊時,我們透過找到的所有增廣路,將除源匯點以外的所有點平衡流量,那麼這個有源匯上下界模型就是可行的。此時只剩下流出大於流入的源點,流入大於流出的匯點。只要我們把新加的這條邊,當做平衡源匯點的一條新路徑,那麼整張圖就相當於無源匯的可行流。那麼反過來,如果這張無源匯圖我們知道有可行流,那麼我們撤銷掉新加的無窮大邊的流量,原圖就會變成一個除源點和匯點以外其他點流量平衡的圖,即有源匯上下界可行流。

有源匯上下界最大流(最小流)

直接上圖:

sxj3.png

這是一個有源點和匯點的圖,雖然看上去有個環,不過我們關注的還是隻從源點流到匯點的最大最小流量。

求最大流的前提是,我們需要先讓這張圖成為可行流:發現點 3 是至少有一個流入量的,所以它一定還要向左流給源點,這一條流量會使得從源點到匯點的總體流量減一,不過沒辦法,因為這是成為可行流的必要流量。

在我們新建那條匯點到源點的無窮大邊,並求解完一個可行流之後,我們冷靜下來思考,有源匯最大流的本質是什麼,其實就是在其他點流量平衡的前提下,最大化源點的缺失流量(或匯點的飽和流量)。我們還是撤銷掉這條無窮大的邊,那麼這條邊的流量,就成為了源點的缺失量,因為這條邊本身的意義就是一條彌補源匯點流量差的路線(第一個標題所講的路線)。

sxj4.png

透過上圖的流量,我們找到了一個可行流的解,在這個解中,無窮大的那條邊流量是 2,流量平衡的過程改變了原圖三條邊的剩餘流量。此時,如果我們撤銷無窮大的邊,那麼源點的流入流出之差會變成 3,這其實就是一個可行流的流量。

那麼聰明的你應該想到了,最大流該如何計算呢,就是在原圖中,繼續找到一些從源點流到匯點的路線,將源點的缺失量進一步擴大。即:s->1->2->t,流量為 3,此時源點流入流出之差為 6,為有源匯最大流。最小值則相反,找到一些從匯點到源點的路線,即:t->3->s,此時源點缺失量被彌補了一些,流入流出之差變成了 1,為有源匯最小流。


總結求解有源匯上下界最大最小流的步驟:

一、新圖跑虛擬源點到虛擬匯點的最大流,判斷是否有無源匯可行流。

二、將無窮大那條邊的流量返還源匯點,得到有源匯可行流。

三、在原圖中繼續跑最大流,再加上可行流的流量(無窮大邊的流量),得到答案(正著跑得到最大流,倒著跑得到最小流)。

模板:luogu東方文花帖

https://www.luogu.com.cn/problem/P5192

這題就是求個有源匯上下界最大流,很裸的模板,不過要先被傳一波教才能讀懂題意。

下面的程式碼中 a 陣列存了原圖的每一條邊,包括上界和下界的資訊。然後 solve 函式就是有源匯上下界最大流的全部過程了,包括建邊,和上面總結的三個步驟。

#include <bits/stdc++.h>
#define ll long long

using namespace std;
const ll N=501010;
const ll inf=0x3f3f3f3f;

inline ll read() {
	ll sum = 0, f = 1; char c = getchar();
	while(c<'0' || c>'9') { if(c=='-') f = -1; c = getchar(); }
	while(c>='0'&&c<='9') { sum = sum * 10 + c - '0'; c = getchar(); }
	return sum * f;
}

ll n,m,s,t,ss,tt;
struct EE{ ll u,v,l,r; } a[N]; ll cnta;
ll du[N];
struct E{
	ll to,nxt,cap;
}e[N];
ll cnt = 1;
ll head[N],cur[N];
ll dep[N],vis[N];
queue <ll> q;

inline void add(ll u,ll v,ll w) {
	// cout<<u<<" -> "<<v<<" "<<w<<endl;
	e[++cnt] = (E){ v,head[u],w }; head[u] = cnt;
	e[++cnt] = (E){ u,head[v],0 }; head[v] = cnt;
}

inline bool SPFA(ll S,ll T) {
	for(ll i=0;i<=tt;i++) dep[i] = inf, vis[i] = 0, cur[i] = head[i];
	q.push(S); dep[S] = 0;
	while(!q.empty()) {
		ll u = q.front(); q.pop();
		vis[u] = 0;
		for(ll i=head[u]; i; i=e[i].nxt) {
			ll v = e[i].to;
			if(dep[v] > dep[u] + 1 && e[i].cap) {
				dep[v] = dep[u] + 1;
				if(vis[v]) continue;
				q.push(v);
				vis[v] = 1;
			}
		}
	}
	return dep[T]!=inf;
}

ll DFS(ll u,ll goal,ll flow) {
	ll res = 0, f;
	if(u==goal || !flow) return flow;
	for(ll i=cur[u]; i; i=e[i].nxt) {
		cur[u] = i;
		ll v = e[i].to;
		if(e[i].cap && (dep[v] == dep[u]+1)) {
			f = DFS(v,goal,min(flow-res,e[i].cap));
			if(f) {
				res += f;
				e[i].cap -= f;
				e[i^1].cap += f;
				if(res==flow) break;
			}
		}
	}
	return res;
}

ll solve() {
	ll ans1 = 0, ans2 = 0, cha = 0;
	for(ll i=1;i<=cnta;i++) {
		ll u = a[i].u, v = a[i].v;
		du[u] += a[i].l;
		du[v] -= a[i].l;
		add(u, v, a[i].r-a[i].l);
	}
	ll lim = cnt;
	for(ll u=s;u<=t;u++) {
		if(du[u]>0) add(u,tt,du[u]), cha += du[u];
		if(du[u]<0) add(ss,u,-du[u]);
	}
	add(t,s,inf);
	while(SPFA(ss,tt)) cha -= DFS(ss,tt,inf);
	if(cha) return -1;

	ans1 = e[cnt].cap;

	for(ll i=lim+1;i<=cnt;i++) e[i].cap = 0;
	while(SPFA(s,t)) ans2 += DFS(s,t,inf);

	return ans1+ans2;
}

void chushihua() {
	cnt = 1;
	cnta = 0;
	for(ll i=0;i<=tt;i++) head[i] = du[i] = 0;
}

int main() {
	ll x,y,l,r;
	while(scanf("%lld%lld",&n,&m)!=EOF) {
		chushihua();

		s = 0; t = n+m+1; ss = n+m+2; tt = n+m+3;
		for(ll i=1;i<=m;i++) {
			x = read();
			a[++cnta] = { i+n,t,x,inf };
		}
		for(ll i=1;i<=n;i++) {
			x = read();
			y = read(); a[++cnta] = { s,i,0,y };
			while(x--) {
				y = read()+1; l = read(); r = read();
				a[++cnta] = { i,y+n,l,r };
			}
		}

		cout<<solve()<<"\n\n";
	}

	// n = 3; s = 0, t = 4; ss = 5; tt = 6;
	// a[++cnta] = {s,1,2,4};
	// a[++cnta] = {1,2,0,1};
	// a[++cnta] = {1,3,3,5};
	// a[++cnta] = {2,t,1,5};
	// a[++cnta] = {3,t,1,3};
	// cout<<solve()<<"\n";
	return 0;
}

相關文章