【網路流】有源匯上下界最大流

HinanawiTenshi發表於2021-04-28

傳送門:https://www.luogu.com.cn/problem/P5192

分析

這是一道有源匯上下界最大流的模板題(廢話)。

既然是網路流的問題,故應該先將圖建出來:

根據題目特徵,

  • 我們將少女和每一天看作是圖中的點。

  • 當然,因為每一天都有拍照次數的限制,我們可以加一個源點 \(s\) ,向每一天連邊,這個邊的容量範圍是 \([0,D_i]\)

  • 類似地,因為每個少女都有一個至少拍照張數的限定,所以加一個匯點 \(t\) ,然後讓每個少女向匯點連邊,容量範圍自然是 \([G_i,∞]\)

  • 至於少女和每一天,根據題意,她們之間連邊的容量是 \([L, R]\)

至此,我們便將圖建好了,從圖的特徵以及題意可以看出,這是一個求有源匯上下界最大流的問題。

怎麼解決呢?

如果對約定的記號不熟悉可以看看我的部落格:
這裡

無源匯上下界可行流

首先,先介紹一下無源匯上下界可行流的求法(即:可行迴圈流的求法):

p.s. 如果會的可以直接跳過看下面的部分

對於可行迴圈流中的邊 \((u,v)\) 有如下容量限制: \(\underline{c}(u,v)\le f(u,v) \le \overline{c}(u,v)\)

簡單的想法是將問題轉化為求有源匯最大流。轉化的方法自然是構造,構造的原則是保證問題的等價。

構造方式:

加入虛擬源點 \(S\) ,虛擬匯點 \(T\)

對於每個點 \(x\),記它的入邊(假設入點為 \(u\) )對應的容量下界為 \(\underline{c}(u,x)\) ,出邊 (假設出點為 \(v\) )對應的容量下界為 \(\underline{c}(x,v)\)

如果 \(c=\sum \underline{c}(u,x)-\sum \underline{c}(x,v)>0\) ,那麼連邊 \((S,x)\) ,容量為 \(c\)

如果 \(c=\sum \underline{c}(u,x)-\sum \underline{c}(x,v)<0\) ,那麼連邊 \((x,T)\) ,容量為 \(-c\)

根據上述規則得到新圖 \(G'\)

只需證明:\(G'\) 的最大流與原圖 \(G\) 的可行流是一一對應的。

證明一個流對應另一個流的方法:從一個流進行等價變換,所得到的新流仍然合法:即滿足容量限制,流量守恆

容量限制較容易證明,故本文出現的證明都是關於證明流量守恆的。

證明:

先證明 \(G\) 的一個可行流對應著 \(G'\) 的一個最大流(這裡的最大流要求源點、匯點與其它點的連邊滿流,在接下來的討論中可以發現需要這個約束)。

約定:點 \(x\) 的入點構成的集合為 \(U\) ,出點構成的集合為 \(V\) ,這樣做可以省去求和 \(\sum\) ,表述方便,即 \(\sum \underline{c}(u,x)=c(U,x)\)
\(\sum \underline{c}(x,v)=c(x,V)\) 等等。

\(G\) 的一個可行流,自然有: \(f(U,x)=f(x,V)\)

等價變形: \(f'(U,x)+\underline{c}(U,x)=f'(x,V)+\underline{c}(x,V)\)

進而有:\(f'(U,x)+\underline{c}(U,x)-\underline{c}(x,V)=f'(x,V)\)

不妨設 \(\underline{c}(U,x)-\underline{c}(x,V)>0\) , 注意到 \(\underline{c}(U,x)-\underline{c}(x,V)\) 恰好是 \(c(S,x)\)\(\underline{c}(U,x)-\underline{c}(x,V)<0\) 的情況同理,故對於新圖,流量也是守恆的。

而且從中可以發現,這裡的最大流要求源點、匯點與其它點的連邊滿流

從上面的證明可以發現 \(G'\) 的一個最大流也對應著 \(G\) 的一個可行流,只需要將公式倒著寫一遍即可(因為是恆等變換)。

至此,證明結束。

在具體操作的時候,要注意跑完最大流之後,對應的邊需要加上補償值才是所要求的網路(參見證明中的公式以及下面的程式碼)。

求取可行迴圈流具體過程可以見程式碼:

#include<bits/stdc++.h>
using namespace std;

const int N=205, M=12005+N<<1;
const int INF=0x3f3f3f3f;

int n, m, S, T;
struct node{
    int to, c, l, next;
}e[M];
int h[N], tot;

void add(int u, int v, int lc, int uc){
    e[tot].to=v, e[tot].c=uc-lc, e[tot].l=lc, e[tot].next=h[u], h[u]=tot++;
    e[tot].to=u, e[tot].c=0, e[tot].next=h[v], h[v]=tot++;
}

int imo[N]; // in minus out

int d[N], q[N], cur[N];

bool bfs(){
    memset(d, -1, sizeof d);
    int tt=-1, hh=0;
    q[++tt]=S, d[S]=0, cur[S]=h[S];
    while(tt>=hh){
        int hd=q[hh++];
        for(int i=h[hd]; ~i; i=e[i].next){
            int go=e[i].to;
            if(d[go]==-1 && e[i].c){
                cur[go]=h[go];
                d[go]=d[hd]+1;
                if(go==T) return true;
                q[++tt]=go;
            }
        }
    }
    return false;
}

int find(int u, int limit){
    if(u==T) return limit;
    int flow=0;
    for(int i=cur[u]; ~i && limit>flow; i=e[i].next){
        int go=e[i].to;
        cur[u]=i;
        if(e[i].c && d[go]==d[u]+1){
            int t=find(go, min(limit-flow, e[i].c));
            if(!t) d[go]=-1;
            flow+=t, e[i].c-=t, e[i^1].c+=t;
        }
    }
    return flow;
}

int dinic(){
    int res=0, flow;
    while(bfs()) while(flow=find(S, INF)) res+=flow;
    return res; 
}

int main(){
    memset(h, -1, sizeof h);
    cin>>n>>m;
    S=0, T=n+1;
    for(int i=1; i<=m; i++){
        int u, v, lc, uc; cin>>u>>v>>lc>>uc;
        add(u, v, lc, uc);
        imo[u]-=lc, imo[v]+=lc;
    }   

    int tot=0;
    for(int i=1; i<=n; i++)
        if(imo[i]>0) add(S, i, 0, imo[i]), tot+=imo[i];
        else if(imo[i]<0) add(i, T, 0, -imo[i]);

    if(dinic()!=tot) puts("NO");
    else{
        puts("YES");
        for(int i=0; i<2*m; i+=2) cout<<e[i].l+e[i^1].c<<endl;
    }
    return 0;
}

接下來求有源匯上下界最大流

在上面,我們已經知道了怎樣求無源匯上下界可行流,那先看看有源匯上下界可行流怎麼求:方法其實很簡單,將匯點 \(t\) 向源點 \(s\) 連一條容量 \(∞\) 的邊(我們稱之為虛邊)即可,有個直觀的比喻:就像是在匯點 \(t\) 連線一臺水泵,將源點 \(s\) 流往 匯點 \(t\) 的水重新泵上去。這樣做就將問題轉換為了熟悉的無源匯上下界可行流問題。

簡單來說:

\(t\) 向源點 \(s\) 連一條容量 \(∞\) 的邊,得到迴圈流。

從虛擬源點 \(S\)\(T\) 跑一遍最大流。

記錄 \(t\)\(s\) 連的邊的流量(即虛邊流量 \(res_1\) ),然後斷開虛邊,從 \(s\)\(t\) 跑一遍最大流得到新增的流量 \(res_2\) ,答案就是 \(res_1+res_2\)

注意到,無源匯上下界可行流對應的流網路與虛擬源匯點 \(S,T\) 相連的邊都是滿流,所以在跑完一遍最大流之後,與虛擬源匯點 \(S,T\) 相連的邊都不可能再被用到,我們可以直接將它們全部拆掉,然後,再將虛邊(水泵)拆掉,發現只需要加回補償值(但事實上,因為 \(t\)\(s\) 連的邊容量範圍是 \([0,∞)\) ,不需要進行補償),所得到的流就是一個合法的有源匯上下界可行流了。

也就是說新圖的可行流和原圖(沒有經過變換的,最原始的)的可行流是一一對應的,那麼新圖的最大流也必然是對應著原圖的最大流了。

因此,最後我們從 \(s\)\(t\) 跑一遍最大流即可。

程式碼:

#include<bits/stdc++.h>
using namespace std;

const int N=1550, M=365+1000+365*1000+N<<1, INF=0x3f3f3f3f; 

int n, m, s, t, S, T;
struct node{
	int to, c, next;
}e[M];
int h[N], tot;

// 連邊函式,有這樣的性質:正向邊^1=反向邊
void add(int u, int v, int c){
	e[tot].to=v, e[tot].c=c, e[tot].next=h[u], h[u]=tot++;
	e[tot].to=u, e[tot].c=0, e[tot].next=h[v], h[v]=tot++;
}

int imo[N]; // imo 的意思是: in minus out 入減去出

int q[N], d[N], cur[N];
bool bfs(){
	memset(d, -1, sizeof d);
	int tt=-1, hh=0;
	q[++tt]=S, d[S]=0, cur[S]=h[S];
	while(tt>=hh){
		int hd=q[hh++];
		for(int i=h[hd]; ~i; i=e[i].next){
			int go=e[i].to;
			if(d[go]==-1 && e[i].c){
				d[go]=d[hd]+1;
				cur[go]=h[go];
				if(go==T) return true;
				q[++tt]=go;
			}
		}
	}
	return false;
}

int find(int u, int limit){
	if(u==T) return limit;
	int flow=0;
	for(int i=cur[u]; ~i && limit>flow; i=e[i].next){
		int go=e[i].to;
		cur[u]=i; // 當前弧優化
		if(d[go]==d[u]+1 && e[i].c){
			int t=find(go, min(e[i].c, limit-flow));
			if(!t) d[go]=-1;
			e[i].c-=t, e[i^1].c+=t, flow+=t;
		}
	}
	return flow;
}

int dinic(){
	int res=0, flow;
	while(bfs()) while(flow=find(S, INF)) res+=flow;
	return res;
}

int main(){
	while(cin>>n>>m){
		memset(h, -1, sizeof h), tot=0;
		memset(imo, 0, sizeof imo);
		
		s=0, t=n+m+1, S=n+m+2, T=S+1;
		for(int i=1; i<=m; i++){
			int G; cin>>G;
			add(i, t, INF-G);
			imo[i]-=G, imo[t]+=G;
		}
		
		for(int i=m+1; i<=m+n; i++){
			int C, D; cin>>C>>D;
			add(s, i, D); // 範圍是 [0,D] 因此不需要更新 imo[]
			while(C--){
				int id, lc, uc; cin>>id>>lc>>uc;
				id++;
				add(i, id, uc-lc);
				imo[i]-=lc, imo[id]+=lc;
			}
		}
		
		int cnt=0;
		for(int i=0; i<=n+m+1; i++)
			if(imo[i]>0) add(S, i, imo[i]), cnt+=imo[i];
			else if(imo[i]<0) add(i, T, -imo[i]);
		
		add(t, s, INF); // 新增“水泵”
		
		if(dinic()!=cnt) cout<<-1<<endl<<endl;
		else{
			int res=e[tot-1].c;
			e[tot-1].c=0, e[tot-2].c=0;
			S=s, T=t;
			res+=dinic();
			cout<<res<<endl<<endl;
		}
	}
	return 0;
}

相關文章