1. 程式人生 > 其它 >【筆記】二分圖/網路流

【筆記】二分圖/網路流

來自\(\texttt{SharpnessV}\)省選複習計劃中的二分圖/網路流


【模板】二分圖最大匹配

給定一個二分圖,需要找出最多的不相交的邊。

比較簡單的方法是匈牙利演算法,每次找增廣路然後直接增廣即可。時間複雜度是\(\rm O(NM)\)

#include<cstdio>
#include<cstring>
using namespace std;
struct edge{
	int next;
	int to;
}e[1000000];
int n,m,k,h[5005],to[5005],pop=0;
int visit[5005];
void add(int x,int y){
	pop++;
	e[pop].next=h[x];
	e[pop].to=y;
	h[x]=pop;
}
bool find(int p){
	for(int i=h[p];i;i=e[i].next){
		if(visit[e[i].to])continue;
		visit[e[i].to]=1;
		if(!to[e[i].to]||find(to[e[i].to])){
		  to[e[i].to]=p;
		  return true;
		}
	}
	return false;
}
int main()
{
	scanf("%d%d%d",&n,&m,&k);
	for(int i=1;i<=k;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		if(x<=n&&y<=m)add(x,y);
	}
	int ans=0;
	for(int i=n;i>=1;i--){
		memset(visit,0,sizeof(visit));
		if(find(i))ans++;
	}
	printf("%d\n",ans);
	return 0;
}

複雜一點的方法是直接建圖跑網路流,\(\texttt{Dinic}\) 跑二分圖的時間複雜度是 \(\rm O(N\sqrt{M})\)

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define N 1005
#define M 100005
using namespace std;
int n,m,k,h[N],tot=1;
struct edge{
	int to,nxt,cap;
}e[M<<1];
void add(int x,int y,int z){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].cap=z;e[tot].to=y;
}
int s,t,d[N],cur[N];
queue<int>q;
bool bfs(){
	memset(d,0,sizeof(d));
	d[s]=1;q.push(s);
	while(!q.empty()){
		int x=q.front();q.pop();
		cur[x]=h[x];
		for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&!d[e[i].to])
			d[e[i].to]=d[x]+1,q.push(e[i].to);
	}
	return d[t];
}
int dfs(int x,int flow){
	if(x==t)return flow;
	int res=flow;
	for(int &i=cur[x];i;i=e[i].nxt)
		if(res&&e[i].cap&&d[x]+1==d[e[i].to]){
			int now=dfs(e[i].to,min(res,e[i].cap));
			if(!now)d[e[i].to]=0;
			e[i].cap-=now;
			e[i^1].cap+=now;
			res-=now;
		}
	return flow-res;
}
int main(){
	scanf("%d%d%d",&n,&m,&k);
	s=n+m+1;t=n+m+2;
	rep(i,1,n)add(s,i,1),add(i,s,0);
	rep(i,1,m)add(n+i,t,1),add(t,n+i,0);
	rep(i,1,k){
		int x,y;scanf("%d%d",&x,&y);
		add(x,n+y,1);add(n+y,x,0);
	}
	int ans=0;
	while(bfs())ans+=dfs(s,0x7fffffff);
	printf("%d\n",ans);
	return 0;
} 

【模板】網路最大流

仍然是每次找增廣路然後增廣。每次先 \(\texttt{BFS}\) 出每個節點的層數,然後在分層圖上增廣。因為分了層,所以可以在找到路徑的同時增廣。

時間複雜度是\(\rm O(N^2M)\),一般卡不滿。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 205
#define M 10005
#define int long long 
using namespace std;
int n,m,s,t,h[N],tot=1;
struct edge{
	int to,nxt,cap;
}e[M];
void add(int x,int y,int z){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].cap=z;
	e[++tot].nxt=h[y];h[y]=tot;e[tot].to=x;e[tot].cap=0;
}
int d[N],cur[N];queue<int>q;
bool bfs(){
	memset(d,0,sizeof(d));
	d[s]=1;q.push(s);
	while(!q.empty()){
		int x=q.front();q.pop();
		cur[x]=h[x];
		for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&!d[e[i].to])
			d[e[i].to]=d[x]+1,q.push(e[i].to);
	}
	return d[t]>0;
}
int dfs(int x,int flow){
	if(x==t)return flow;
	int res=flow;
	for(int &i=cur[x];i;i=e[i].nxt){
		if(d[x]+1==d[e[i].to]&&e[i].cap){
			int now=dfs(e[i].to,min(res,e[i].cap));
			if(!now){d[e[i].to]=0;}
			e[i].cap-=now;e[i^1].cap+=now;res-=now;
		}
		if(!res)return flow;
	}
	return flow-res;
}
signed main(){
	scanf("%lld%lld%lld%lld",&n,&m,&s,&t);
	rep(i,1,m){
		int x,y,z;scanf("%lld%lld%lld",&x,&y,&z);
		add(x,y,z);
	}
	long long ans=0;
	while(bfs())ans+=dfs(s,0x7fffffffffffffffLL);
	printf("%lld\n",ans);
	return 0;
}

【模板】最小費用最大流

由於要使得費用最小,所以我們每次找費用最小的增廣路,這樣就不能再使用\(\texttt{Dinic}\)演算法。

退一步,我們用 \(\texttt{EK}\) 演算法,用 \(\texttt{SPFA}\) 找費用最小的增廣路,然後增廣。

時間複雜度能過。一般最大流不卡 \(\texttt{Dinic}\) ,費用流不卡 \(\texttt{EK}\),如果卡了噴出題人就完事了。

#include<bits/stdc++.h>
#define rep(i,a,b) for(int i=a;i<=b;i++)
#define pre(i,a,b) for(int i=a;i>=b;i--)
#define N 5005
#define M 100005
using namespace std;
int n,m,s,t,h[N],tot=1;
struct edge{
	int to,nxt,cap,val;
}e[M];
void add(int x,int y,int z,int val){
	e[++tot].nxt=h[x];h[x]=tot;e[tot].to=y;e[tot].cap=z;e[tot].val=val;
	e[++tot].nxt=h[y];h[y]=tot;e[tot].to=x;e[tot].cap=0;e[tot].val=-val;
}
queue<int>q;
int d[N],pre[N],ff[N];bool v[N];
bool spfa(){
	memset(d,0x3f,sizeof(d));
	memset(v,0,sizeof(v));
	memset(ff,0,sizeof(ff));
	q.push(s);d[s]=0;ff[s]=0x7fffffff;
	while(!q.empty()){
		int x=q.front();q.pop();v[x]=0;
		for(int i=h[x];i;i=e[i].nxt)if(e[i].cap&&e[i].val+d[x]<d[e[i].to]){
			d[e[i].to]=d[x]+e[i].val,pre[e[i].to]=i^1,ff[e[i].to]=min(ff[x],e[i].cap);
			if(!v[e[i].to])v[e[i].to]=1,q.push(e[i].to);
		}
	}
	if(d[t]<0x3f3f3f3f)return true;return false;
}
int flow,ans;
void updata(){
	flow+=ff[t];ans+=ff[t]*d[t];
	int now=t;while(now!=s){e[pre[now]].cap+=ff[t],e[pre[now]^1].cap-=ff[t];now=e[pre[now]].to;}
}
int main(){
	scanf("%d%d%d%d",&n,&m,&s,&t);
	rep(i,1,m){
		int x,y,z,op;scanf("%d%d%d%d",&x,&y,&z,&op);
		add(x,y,z,op);
	}
	while(spfa())updata();
	printf("%d %d\n",flow,ans);
	return 0;
}

以下是正文。

Part 1: 網路流24

Link

P2756 飛行員配對方案問題

二分圖模板,兩種飛行員相互連邊即可。

程式碼

P4016 負載平衡問題

費用流模板,相鄰點連費用為 \(1\) 的邊。

程式碼

P1251 餐巾計劃問題

拆點,將每天拆成兩個點,分別表示這天的新餐巾和這天的舊餐巾。

源點向舊餐巾連容量 \(r_i\),費用為 \(0\) 表示每天產生的舊餐巾。

源點向新餐巾連容量 \(r_i\),費用為 \(p\) 表示購買的新餐巾。

舊餐巾向 \(m/n\) 天后的新餐巾連容量無限,費用為\(f/s\)表示洗餐巾。

新餐巾向匯點連容量 \(r_i\),費用為 \(0\) 表示每天需要的餐巾。

\(i\) 天的舊餐巾向第 \(i+1\) 天的舊餐巾連容量無限,費用 \(0\) 的邊表示今天的餐巾可以拖到明天。

最後跑費用流即可。

程式碼

P2754 [CTSC1999]家園 / 星際轉移問題

按時間建立分層圖,然後跑最大流。

程式碼

P2762 太空飛行計劃問題

最小割即最大流,證明略。

對於本題,源點向實驗連容量為利潤的邊,器材向匯點連容量為費用的邊,相關的實驗和器材之間連 \(\inf\) 的邊。

當我們割掉一條邊,意味著放棄實驗/購買器材。如果存在一條由源點到匯點的路徑,意味著有一個實驗沒有放棄,但是器材仍沒有購買。所以我們要花費最小的代價使得圖不連通,直接跑最小費用最大流。

程式碼

P2763 試題庫問題

簡單網路流建模,但是要輸出方案。

由於最大流等於最小割,所以被流滿的邊就是割集中的邊,就是我們選的試題。

程式碼

P2764 最小路徑覆蓋問題

拆點,原 DAG 上一個點拆為入點和出點,原圖的邊 \(u\to v\),轉換為 \(u_{out}\to v_{in}\),源點向入點連邊,出點向匯點連邊,割掉的一條邊表示合併原來的兩條路徑。

不是很難理解,最後輸出方案需要用到並查集。

程式碼

P2766 最長不下降子序列問題

拆點,對於在點上的限制,例如限制一個點的選取次數,我們可以將點拆為兩個點,然後在點之間連邊。

本題拆點,然後對於\(f[i]+1=f[j]\)的轉移,在\(i\)的出點和\(j\)的入點間連邊。

程式碼

P3355 騎士共存問題

建模不難,這是個二分圖最大獨立集。

二分圖中:最大獨立集 \(=\) 點數 \(-\) 最小點覆蓋,最小點覆蓋 \(=\) 最大匹配 \(=\) 最小割 \(=\) 最大流 。

程式碼

Part 2:省選

P2423 [HEOI2012]朋友圈

顯然補圖的最大獨立集等於原圖的最大團,補圖的最大團等於原圖的最大獨立集。

二分圖的最大獨立集等於點數減去最小覆蓋,最小覆蓋等於最大匹配。

程式碼

P2825 [HEOI2016/TJOI2016]遊戲

如果沒有牆,就是經典的行列模型,直接上二分圖。

既然有牆,我們仍然可以看作行列模型。只不過如果有牆阻擋,就把原來的行/列拆成多段,然後二分圖匹配即可。

程式碼

P3731 [HAOI2017]新型城市化

求二分圖最大匹配必經邊。

我們可以先跑網路流,得到殘餘網路。

殘餘網路包括很多資訊,比如退流的資訊。

那麼必經邊的兩段在原圖上必定不強連通,因為如果強連通,則必然包含一個環,我們可以將環上的一條邊退流,從環的其餘部分增廣。

所以我們再在殘餘網路上跑一邊\(\texttt{Tarjan}\)演算法即可。

程式碼

P3749 [六省聯考2017]壽司餐廳

經典模型:最大權閉合子圖。

給定若干個物品,每個物品有一個價值,以及一些限制條件\(u\to v\)表示選了物品\(u\)就必須選物品\(v\),求最大價值和。

這個簡單:我全部選

價值可以為負。

我們可以將模型轉換為最小割模型,對於每個物品,如果點權大於\(0\),與\(S\)連邊,否則與\(T\)連邊,容量為價值的絕對值。

對於一個條件\(u\to v\),從 \(u\)\(v\) 連容量為 \(\inf\) 的邊。

最後跑最大流最小割即可。

分析一下,對於每個物品,如果割掉連 \(S\) 的邊,表示不選它,割掉和\(T\)的邊,表示選它。

那麼如果存在一條通路\(S\to a\to b\to T\),表示沒有選擇了\(a\),而沒有選擇\(b\)。最小割可以使得網路中不存在通路。

程式碼

P2805 [NOI2009] 植物大戰殭屍

同樣是最大權閉合子圖,難度低於上面的題,留給思考。

程式碼

P2053 [SCOI2007]修車

很好的思維題。

拆點,對於每個師傅,我們拆乘\(N\)個點,第\(i\)個點表示是倒數第\(i\)個來修車的。

對於一輛車,如果它是倒數第\(i\)個修的,那麼它的修車時間要算\(i\)次。所以費用為修車時間\(\times i\) ,容量為\(1\),拆出的每個點連向匯點的邊容量為\(1\),表示一個師傅一個時間只能修一個車。

程式碼

P4043 [AHOI2014/JSOI2014]支線劇情

上下界最小費用可行流。

對於每條必須邊,先把它流滿。為使整張網路的流量守恆,我們再建立超級源點和超級匯點進行補流操作。

我們對每個點計算 \(d[i]\) 表示將必經邊流滿後第\(i\)個節點入流和出流的差。

如果\(d[i]>0\)說明供大於求,我們從超級源點向\(i\)連一條容量為\(d[i]\)的邊,表示還需要吐出這麼多流。

如果\(d[i]<0\)說明供不應求,我們從\(i\)向超級匯點連一條容量為\(-d[i]\)的邊,表示還需要吞掉這麼多流。

我們還要連\(t\to s\)的容量為\(\inf\)的邊,表示整張圖的入流和出流平衡。

程式碼

上下界網路流還有一系列,但本質上相同。