1. 程式人生 > 其它 >網路流題目選解

網路流題目選解

本文主要講解了部分網路流題目。

這裡有板子

最大流

view code
namespace Flow
{
	int tot=1,fi[N],ne[M],to[M],w[M],S,T,d[N],nn;
	inline void add(int x,int y,int c)
	{
		ne[++tot]=fi[x],fi[x]=tot,to[tot]=y,w[tot]=c;
		ne[++tot]=fi[y],fi[y]=tot,to[tot]=x,w[tot]=0;
	}
	inline bool bfs()
	{
		fill(d+1,d+nn+1,-1);d[S]=0;
		queue<int>q;q.push(S);
		while(!q.empty())
		{
			int u=q.front();q.pop();
			for(int i=fi[u];i;i=ne[i])
			{
				int v=to[i];
				if(d[v]<0&&w[i])
					d[v]=d[u]+1,q.push(v);
			}
		}
		return d[T]!=-1;
	}
	int dfs(int u,int flow)
	{
		if(!flow||u==T)return flow;
		int used=0;
		for(int i=fi[u];i;i=ne[i])
		{
			int v=to[i];
			if(d[v]!=d[u]+1)continue;
			int t=dfs(v,min(w[i],flow-used));
			w[i]-=t,w[i^1]+=t;
			used+=t;
			if(used==flow)break;
		}
		if(used<flow)d[u]=-1;
		return used;
	}
	inline int dinic()
	{
		int e=0;
		while(bfs())e+=dfs(S,inf);
		return e;
	}
}
using namespace Flow;

費用流

view code
namespace Flow
{
	int tot=1,S,T,nn,fi[N],ne[M],to[M],c[M],pot[N],pre[N],f[N],w[M],dis[N];bool inq[N];
	inline void add(int x,int y,int s,int wt)
	{
		ne[++tot]=fi[x],fi[x]=tot,to[tot]=y,c[tot]=s,w[tot]=wt;
		ne[++tot]=fi[y],fi[y]=tot,to[tot]=x,c[tot]=0,w[tot]=-wt;
	}
	inline bool spfa()
	{
		fill(dis+1,dis+nn+1,-inf);dis[S]=0;
		queue<int>q;q.push(S);inq[S]=1;f[S]=inf;
		while(!q.empty())
		{
			int u=q.front();q.pop();inq[u]=0;
			for(int i=fi[u];i;i=ne[i])
			{
				int v=to[i];
				if(c[i]&&dis[v]<dis[u]+w[i])
				{
					dis[v]=dis[u]+w[i];
					pre[v]=u,pot[v]=i^1;
					f[v]=min(c[i],f[u]);
					if(!inq[v])q.push(v),inq[v]=1;
				}
			}
		}
		return dis[T]!=-inf;
	}
	inline pair<int,int> dinic()
	{
		int cost=0;int F=0;
		while(spfa())
		{
			int now=T,flow=f[T];F+=flow;
			while(now!=S)
			{
				cost+=w[pot[now]^1]*flow;
				c[pot[now]]+=flow;
				c[pot[now]^1]-=flow;
				now=pre[now];
			}
		}
		return make_pair(F,cost);
	}
}
using namespace Flow;

Part 1

一類網路流題目滿足如下性質:

  • 有若干元素,每個元素有兩種狀態(黑或白,選或不選···),要求選出其中一種,且選擇某一種有相應收益
  • 有若干限制或者額外收益,類似選擇了\(A\)\(X\) 狀態就不能選擇\(B\)\(Y\) 狀態或同時選擇\(A\)\(X\) 狀態和\(B\)\(Y\) 狀態帶來\(w\) 的額外收益。
  • 最後要求出收益最大值。

這種問題的套路都是先全部選擇所有收益,然後減去構造出來的圖的最小割(意義是不得不放棄的最小代價)(在此最小割中,在源點集合的選擇一種狀態,而在匯點集合的選擇另外一種狀態),即為最終答案。

這麼說可能有些抽象,下面的題目均是此型別,便於理解。

[國家集訓隊]happiness

description

一個\(n\times m\) 座位表,每個座位上有一個同學。對於每個同學,選擇文科或理科各有一定喜悅值。同時,對於位置相鄰的兩個同學,如果他們同時選擇文科或理科各會帶來額外的喜悅值。求最大喜悅值。

solution

考慮如何用最小割表示不得不放棄的最小代價

建立超級源點和超級匯點分別代表文科和理科。對於每個同學分別建點,如果在最小割中屬於源點集合則代表他選擇文科,否則選擇理科。

源點向每個同學連線容量為選擇文科喜悅值的邊,每個同學向匯點連線容量為選擇理科喜悅值的邊。這樣割去源點的邊代表放棄文科帶來的權值,最終選擇了理科;割去匯點的邊同理。

而後考慮額外權值。仍然是套路地,對於每一個額外權值建立新點,如果這個權值和選擇文科相關,則由源點向它連線容量為權值的邊,由它向對應同學連線容量為\(+\infty\) 的邊。這樣如果其中某位同學最終屬於匯點集合(相當於選擇理科),那麼由匯點連出的邊必然會斷去,代表放棄了這個額外權值。

最終答案為初始權值和減去最小割。

code

view code
int n,m,ans,o;
inline int id(int x,int y){return (x-1)*n+y+2;}
int main()
{
	scanf("%d%d",&n,&m);S=1,T=2;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			scanf("%d",&o),ans+=o,add(S,id(i,j),o);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			scanf("%d",&o),ans+=o,add(id(i,j),T,o);
	nn=n*m+2;
	for(int i=1;i<n;++i)
		for(int j=1;j<=m;++j)
		{
			int p=++nn;
			scanf("%d",&o);ans+=o;
			add(S,p,o),add(p,id(i,j),inf),add(p,id(i+1,j),inf);
		}
	for(int i=1;i<n;++i)
		for(int j=1;j<=m;++j)
		{
			int p=++nn;
			scanf("%d",&o);ans+=o;
			add(p,T,o),add(id(i,j),p,inf),add(id(i+1,j),p,inf);
		}
	for(int i=1;i<=n;++i)
		for(int j=1;j<m;++j)
		{
			int p=++nn;
			scanf("%d",&o);ans+=o;
			add(S,p,o),add(p,id(i,j),inf),add(p,id(i,j+1),inf);
		}
	for(int i=1;i<=n;++i)
		for(int j=1;j<m;++j)
		{
			int p=++nn;
			scanf("%d",&o);ans+=o;
			add(p,T,o),add(id(i,j),p,inf),add(id(i,j+1),p,inf);
		}
	printf("%d\n",ans-dinic());
	return 0;
}

方格取數問題

solution

這個是有限制的情況。

根據套路,先將所有的權值都選出來,然後用最小的代價刪去一些點使得剩下的可行。

再轉化一下,我們將一個方格和它相鄰的方格連邊,那麼問題轉化為了圖的帶權最大獨立集。

根據黑白染色可以發現,這個圖一定是一個二分圖。這樣的話就可做了。

由源點向黑點連邊,容量為其權值;白點向匯點連邊,容量為其權值。黑點連向相鄰的白點,容量為\(+\infty\) 。這樣任意一條從源點到匯點的路徑就是一種不合法的情況,我們只需話最小代價割邊使其不連通,符合最小割的定義。於是就做完了。

code

view code
int n,m,ans;
const int fx[]={0,0,1,-1},fy[]={1,-1,0,0};
inline bool ok(int x,int y){return x>=1&&x<=n&&y>=1&&y<=m;}
inline int id(int x,int y){return (x-1)*m+y;}
int main()
{
	scanf("%d%d",&n,&m);nn=n*m;
	S=++nn,T=++nn;int d;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
		{
			scanf("%d",&d);ans+=d;
			int now=id(i,j);
			if((i+j)&1)add(S,now,d);
			else{add(now,T,d);continue;}
			for(int k=0;k<4;++k)
			{
				int x=i+fx[k],y=j+fy[k];
				if(ok(x,y))add(now,id(x,y),inf);
			}
		}
	printf("%d\n",ans-dinic());
	return 0;
}

[BJOI2016]水晶

solution

原題座標很煩,因為座標和位置並不是一一對應的。於是可以這樣轉化\((x,y,z)\rightarrow(x-z,y-z)\) (原因:向量加法遵從平行四邊形定則)。平面鈍角座標系???

注意到這樣轉化後仍然有\(x+y+z\equiv x-z+y-z\pmod 3\)

不難發現\(a\) 共振和\(b\) 共振一起相當於不能存在相鄰的三個位置\((x_1,y_1),(x_2,y_2),(x_3,y_3)\) 滿足

\[x_1+y_1\equiv 1\pmod 3\\ x_2+y_2\equiv 0\pmod 3\\ x_3+y_3\equiv 2\pmod 3\\ \]

於是對於每個水晶先拆點,分為入點和出點,由入點向出點連容量為權值的邊。

然後將水晶按照橫縱座標數值之和除以\(3\) 的餘數分類。模\(3\)\(1\) 的向相鄰的模\(3\)\(0\) 的連邊,模\(3\)\(0\) 的向相鄰的模\(3\)\(2\) 的連邊,容量均為\(+\infty\)

然後源點向模\(3\)\(1\) 的點連邊,模\(3\)\(2\) 的點向匯點連邊,容量均為\(+\infty\)

和上一道題類似的分析方法,最終答案為總權值減取最小割。

注意:1.此題中相鄰和別的題目中的定義略有不同。2.同一個點中有多個水晶的情況需要特殊處理。

code

view code
const int fx[]={0,0,1,-1,1,-1},fy[]={1,-1,0,0,1,-1};
map<pair<int,int>,int>mp,buc;
int x[P],y[P],tp[P],n,ans;
int main()
{
	scanf("%d",&n);
	for(int i=1,z,v;i<=n;++i)
	{
		scanf("%d%d%d%d",&x[i],&y[i],&z,&v);
		x[i]-=z,y[i]-=z;
		tp[i]=(x[i]+y[i])%3;tp[i]=(tp[i]+3)%3;
		v*=10;if(!tp[i])v=v/10*11;ans+=v;
		buc[make_pair(x[i],y[i])]+=v;
	}
	n=buc.size();int now=0;
	for(auto it=buc.begin();it!=buc.end();it++)
	{
		++now,add(now,now+n,it->second);
		x[now]=(it->first).first,y[now]=(it->first).second;
		tp[now]=(x[now]+y[now])%3;tp[now]=(tp[now]+3)%3;
		mp[it->first]=now;
	}
	nn=2*n;S=++nn;T=++nn;
	for(int i=1;i<=n;++i)
	{
		if(tp[i]==2)add(i+n,T,inf);
		else if(tp[i]==1)
		{
			add(S,i,inf);
			for(int k=0;k<6;++k)
			{
				int nx=x[i]+fx[k],ny=y[i]+fy[k];
				if(!mp.count(make_pair(nx,ny)))continue;
				int id=mp[make_pair(nx,ny)];
				if(tp[id])continue;
				add(i+n,id,inf);
			}
		}
		else
		{
			for(int k=0;k<6;++k)
			{
				int nx=x[i]+fx[k],ny=y[i]+fy[k];
				if(!mp.count(make_pair(nx,ny)))continue;
				int id=mp[make_pair(nx,ny)];
				if(tp[id]!=2)continue;
				add(i+n,id,inf);
			}
		}
	}
	printf("%.1lf",(ans-dinic())/10.0);
	return 0;
}

Part 2

最大權閉合子圖問題 ,它的描述是這樣的:

對於一個點帶權的\(DAG\) 圖,定義其閉合子圖為一個點集\(S\) 滿足如果\(u\in S\) ,則所有\(u\) 能到達的點都屬於\(S\) 。而最大權閉合子圖就是點權和最大的閉合子圖。

如何解決??還是沿襲先前的套路,先選取所有的正權點的權值,然後建立最小割模型,由源點向正權點連線容量為其權值的邊,負權點向匯點連線容量為其權值相反數的邊,然後原圖的邊均保留,容量為\(+\infty\)

最後答案為所有的正權點的權值減去最小割。在最小割中,沒有被割的連向正權點的邊表示選擇了這個點,被割的由負權點連出的邊表示選擇了這個點,不難發現這樣做一定是滿足條件的。

[NOI2009] 植物大戰殭屍

solution

每個植物看作一個點,點權為其收益,如果植物\(a\) 保護植物\(b\) ,則連邊\(b\rightarrow a\) 表示將\(a\) 吃掉後吃掉\(b\)

注意這樣建出來的是有環的,而方才介紹的模型是不允許有環的。於是拓撲排序去除其中的環然後套用上面的模型即可。

注意拓撲排序建出的圖恰好是我們需要建出的圖的反圖,稍微注意下。

code

view code
int n,m,val[P*P],deg[P*P];bool flag[P*P],G[P*P][P*P];
inline int id(int x,int y){return (x-1)*m+y;}
vector<int>e[P*P];
inline void adde(int x,int y){e[x].push_back(y);}
inline void topo()
{
	nn=n*m;
	for(int i=1;i<=nn;++i)
		for(int v:e[i])++deg[v];
	queue<int>q;
	for(int i=1;i<=nn;++i)
		if(!deg[i])q.push(i),flag[i]=1;
	while(!q.empty())
	{
		int u=q.front();q.pop();
		for(int v:e[u])
			if(!(--deg[v]))
				flag[v]=1,q.push(v);
	}
}
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		for(int j=1,w,x,y;j<=m;++j)
		{
			int now=id(i,j);
			scanf("%d%d",&val[now],&w);
			while(w--)
				scanf("%d%d",&x,&y),
				adde(now,id(x+1,y+1));
		}
	for(int i=1;i<=n;++i)
		for(int j=2;j<=m;++j)
			adde(id(i,j),id(i,j-1));
	topo();
	for(int u=1;u<=nn;++u)
	{
		if(!flag[u])continue;
		for(int v:e[u])
			if(flag[v]&&!G[v][u])add(v,u,inf),G[v][u]=1;
	}
	S=++nn,T=++nn;int ans=0;
	for(int u=1;u<=n*m;++u)
	{
		if(!flag[u])continue;
		if(val[u]>0)add(S,u,val[u]),ans+=val[u];
		else add(u,T,-val[u]);
	}
	printf("%d\n",ans-dinic());
	return 0;
}

Part 3

直接看例題。

最長k可重區間集問題

solution

該模型較為經典,因此特寫一篇題解以加深印象。

容易使人聯想到是費用流,但如何建模才是關鍵。我們可以將整個過程做如下轉化:初始時有\(k\) 個人,\(n\) 件工作。每件工作的持續時間為\((l_i,r_i)\) ,僅需要一個人做且這個人做這件工作時不能做其他工作,而完整地做完這件工作可以獲得\(w_i\) 的收益。現在需要你合理地安排使得收益之和最大。該問題只是原問題的另一種形式,本質相同,但是建模卻方便地多:即建立超級源點向數軸上0的位置連邊,流量為\(k\) 費用為0。然後對於數軸上每個點\(i\)\(i+1\) 連邊,流量為\(k\) 費用為0。然後從數軸上最大的點向超級匯點連邊。最後對於每個區間,從\(l_i\)\(r_i\) 連邊,流量為1費用為\(w_i\) 。跑出最大費用可行流即為答案。值域比較大時可以先離散化。

最長k可重線段集問題

solution

本題和上一題本質相同,只有處理垂直於\(x\) 軸線段不太相同。對於這種問題,固然可以直接拆點,但是更加簡單的方式是擴域。相當於我們現在不只有整點,還有諸如\(\dfrac d2\)\(d\) 為奇數)這類點。為了仍保持原來的相交/不相交關係,只需要將\(l=r\)\((l,r)\) 變為\((l,r+0.5)\)\(l\not=r\)\((l,r)\) 變為\((l+0.5,r)\) 即可。然後再將座標同統一\(\times 2\) 就和先前的題完全一致了。

Part 4

一些雜題??

[RC-02] 開門大吉

description

\(n\) 個人,\(m\) 套題,第\(i\) 個人去做第\(j\) 套題會產生\(g_{i,j}\) 的權值。現在還有若干要求,每個要求形如\(i\ j\ k\) 表示第\(i\) 個人做的題的編號至少比第\(j\) 個人做的題目編號大\(k\) 。現在要給每個人分配一套題使得總權值儘量小。

solution

總權值最小聯想到最小割模型。

拆點。將每個人\(i\) 拆為\(m+1\) 個點,連邊\((i,j)\rightarrow(i,j+1)\) 流量為\(g_{i,j}\) ,割去這條邊表示第\(i\) 個人做第\(j\) 套題產生的代價。

另外,因為一個人只能做一套題,為了保證對於一個人只割一條邊,我們還需要連邊\((i,j)\leftarrow (i,j+1)\) 邊權為\(+\infty\) 。這樣的話如果最小割中被割了兩次,則會引出矛盾:

因為是最小割,所以割的兩條邊必須有用,也就是它們前面與源點連通,後面與匯點連通。然而,因為有反向邊,則後面的那個與源點連通,然後就可以走過去,走到匯點,與是一個割矛盾。

然後源點向所有的\((i,1)\) 連邊,流量為\(+\infty\)\((i,m+1)\) 向匯點連邊,流量為\(+\infty\)

現在的關鍵在於如何滿足要求。

對於要求\((i,j,k)\) ,我們需要連邊\((j,x)\rightarrow(i,x+k)(1\le x,x+k\le m+1)\) 流量為\(+\infty\) 。這樣的話如果選擇在\((j,x)\rightarrow (j,x+1)\) 處割掉,那麼由於\(S\rightarrow(j,1)\rightarrow(j,x)\rightarrow(i,x+k)\rightarrow(i,m+1)\rightarrow T\) 這條路徑的存在,則必須在\(\ge x+k\) 的地方割掉,滿足了要求。

code

view code
int n,m,p,y,c[P];
db a[P][P];
inline int id(int x,int y){return (x-1)*(m+1)+y;}
int main()
{
	int t;scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d%d%d",&n,&m,&p,&y);
		for(int i=1;i<=p;++i)scanf("%d",c+i),c[i]+=c[i-1];
		for(int j=1;j<=m;++j)
			for(int i=1;i<=n;++i)
			{
				a[i][j]=0;db now=1.0,x;
				for(int k=1;k<=p;++k)
				{
					scanf("%lf",&x);
					a[i][j]+=c[k-1]*now*(1-x);
					now*=x;
				}
				a[i][j]+=c[p]*now;
			}
		pre(n*(m+1)+2);
		for(int i=1;i<=n;++i)
		{
			add(S,id(i,1),inf),add(id(i,m+1),T,inf);
			for(int j=1;j<=m;++j)
				add(id(i,j),id(i,j+1),a[i][j]),
				add(id(i,j+1),id(i,j),inf);
		}
		while(y--)
		{
			int x,y,k;scanf("%d%d%d",&x,&y,&k);
			for(int i=1;i<=m;++i)
				if(i+k>=1&&i+k<=m+1)
					add(id(y,i),id(x,i+k),inf);
		}
		db ans=dinic();
		if(sig(inf-ans)<=0)puts("-1");
		else printf("%.4lf\n",ans);
	}
	return 0;
}

CF277E Binary Tree on Plane

solution

拆點。一個點拆為入點和出點。

源點向所有入點連線容量為\(2\) 費用為\(0\) 的邊代表該節點至多兩個兒子。

所有出點向匯點連邊,容量為\(1\) 費用為\(0\) 代表該節點至多有一個父親。

然後對於一個點\(i\) ,它的入點向所有可以作為它兒子\(j\) 的出點連線容量為\(1\) ,費用為其距離的邊,代表這條邊至多選擇一起,且代價為其距離。

最後跑最小費用最大流。注意只有根沒有父親,所以當且僅當最大流為\(n-1\) 時才有解。

code

view code
int n,x[P],y[P];
inline int sq(int a){return a*a;}
inline db d(int a,int b){return sqrt(sq(x[a]-x[b])+sq(y[a]-y[b]));}
int main()
{
	scanf("%d",&n);init(2*n+2);
	for(int i=1;i<=n;++i)
		scanf("%d%d",x+i,y+i);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			if(y[j]<y[i])
				add(i,j+n,1,d(i,j));
	for(int i=1;i<=n;++i)
		add(S,i,2,0),add(i+n,T,1,0);
	auto ans=dinic();
	if(ans.first<n-1)puts("-1");
	else printf("%.15lf\n",ans.second);
	return 0;
}

[NOI2012] 美食節

solution

首先需要將總等待時間進行轉化,假設現在只有一個師傅,所需做的菜所花的時間依次為\(t_1,t_2,\cdots t_k\) ,那麼總等待時間為

\[\sum_{i=1}^kt_i+\sum_{i=2}^kt_i+\cdots \sum_{i=k}^kt_i=\sum_{j=1}^k\sum_{i=j}^kt_i=\sum_{i=1}^kt_i(k-i+1) \]

說人話就是如果當前這個菜是倒數第\(p\) 個做的,那麼對總等待時間的貢獻為\(pt\)

於是就可以開開心心地建圖了。記\(sum=\sum_{i=1}^np_i\) ,那麼將每個廚師\(i\) 拆為\(sum\) 個點代表廚師\(i\) 做倒數第\(j\) 個菜。那麼由源點向每一種菜連邊,容量為\(p_i\) ,費用為\(0\) ;每種菜向剛才拆的點(廚師\(j\) 做倒數第\(k\) 個菜)連邊,容量為\(1\),費用為\(t_{j,k}\times k\) ;這些點再向匯點連容量為\(1\) ,費用為\(0\) 的邊。最後跑一個最小費用最大流就是答案。

這個模型可能可以解決不少類似問題吧。

但對於此題來講,直接這樣建圖會\(T\) 。考慮優化。其實我們根本不用拆那麼多點,因為不可能每個廚師都有\(sum\) 個菜做。注意到這樣一個性質:對於同一個廚師,一定是先走倒數第\(k\) 個做菜,然後菜有可能走倒數第\(k+1\) 個做菜。於是乎初始時只用連倒數第一道菜的邊,然後增廣一次我們就檢查每一位廚師看他是否做完了倒數第一道菜,如果是,再增加倒數第二道菜的邊,以此類推。類似於資料結構的動態開點

code

view code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=1e7+5,inf=0x3f3f3f3f,P=105;
int m,n,t[P][P],p[P],sum,now[P],id[P];
namespace Flow
{
	int tot=1,S,T,nn,fi[N],ne[M],to[M],c[M],pot[N],pre[N],f[N],w[M],dis[N];bool inq[N];
	inline void add(int x,int y,int s,int wt)
	{
		ne[++tot]=fi[x],fi[x]=tot,to[tot]=y,c[tot]=s,w[tot]=wt;
		ne[++tot]=fi[y],fi[y]=tot,to[tot]=x,c[tot]=0,w[tot]=-wt;
	}
	inline void adde(int i,int pos)
	{
		int cur=++nn;
		for(int k=1;k<=n;++k)
			add(k+2,cur,inf,t[i][k]*pos);
		add(cur,T,1,0);now[i]=pos;id[i]=cur;
	}
	inline bool spfa()
	{
		fill(dis+1,dis+nn+1,inf);dis[S]=0;
		queue<int>q;q.push(S);inq[S]=1;f[S]=inf;
		while(!q.empty())
		{
			int u=q.front();q.pop();inq[u]=0;
			for(int i=fi[u];i;i=ne[i])
			{
				int v=to[i];
				if(c[i]&&dis[v]>dis[u]+w[i])
				{
					dis[v]=dis[u]+w[i];
					pre[v]=u,pot[v]=i^1;
					f[v]=min(c[i],f[u]);
					if(!inq[v])q.push(v),inq[v]=1;
				}
			}
		}
		return dis[T]!=inf;
	}
	inline pair<int,int> dinic()
	{
		int cost=0,F=0;
		while(spfa())
		{
			int now=T,flow=f[T];F+=flow;
			while(now!=S)
			{
				cost+=w[pot[now]^1]*flow;
				c[pot[now]]+=flow;
				c[pot[now]^1]-=flow;
				now=pre[now];
			}
			for(int u=1;u<=m;++u)
				for(int i=fi[id[u]];i;i=ne[i])
				{
					int v=to[i];if(v!=T)continue;
					if(c[i])continue;
					adde(u,::now[u]+1);break;
				}
		}
		return make_pair(F,cost);
	}
}
using namespace Flow;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)scanf("%d",p+i),sum+=p[i];
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			scanf("%d",&t[j][i]);
	S=1,T=2;
	for(int i=1;i<=n;++i)add(S,i+2,p[i],0);nn=n+2;
	for(int i=1;i<=m;++i)adde(i,1);
	printf("%d\n",dinic().second);
	return 0;
}

[SCOI2012]奇怪的遊戲

solution

黑白染色。假設黑色格子有\(cb\) 個,權值和為\(sb\) ,白色格子有\(cw\) 個,權值和為\(sw\) 。每次操作一定是黑色白色權值各加\(1\)

假設最終的同一個數\(X\) ,那麼就有\(sb-sw=X(cb-cw)\)

\(cb-cw\not=0\) 當且僅當\(n,m\) 均為奇數。此時可以計算出\(X\) 的值,然後最大流隨便\(check\) 一下就行。

否則\(n,m\) 中至少一個為偶數。此時倘若\(X\) 可行,那麼\(X+1\) 必然也可行。於是二分答案,還是用最大流\(check\)

\(check\) 類似於二分圖匹配的過程,最後判斷是否滿流即可。

view code
const int fx[]={0,0,1,-1},fy[]={1,-1,0,0};
int n,m,mp[P][P];ll sb,sw,mx;
inline bool ok(int x,int y){return x>=1&&x<=n&&y>=1&&y<=m;}
inline int id(int x,int y){return (x-1)*m+y;}
inline bool ck(ll x)
{
	clear();ll sum=0;
	S=n*m+1,T=S+1;nn=T;
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
		{
			if(!((i+j)&1)){add(id(i,j),T,x-mp[i][j]);continue;}
			add(S,id(i,j),x-mp[i][j]);sum+=x-mp[i][j];
			for(int k=0;k<4;++k)
			{
				int x=i+fx[k],y=j+fy[k];
				if(ok(x,y))add(id(i,j),id(x,y),inf);
			}
		}
	ll ret=dinic();
	return ret==sum;
}
int main()
{
	int t;scanf("%d",&t);
	while(t--)
	{
		scanf("%d%d",&n,&m);mx=sb=sw=0;
		for(int i=1;i<=n;++i)
			for(int j=1;j<=m;++j)
			{
				scanf("%d",&mp[i][j]);
				((i+j)&1)?sb+=mp[i][j]:sw+=mp[i][j];
				mx=max(mx,0ll+mp[i][j]);
			}
		if((n*m)&1)
		{
			ll ans=sw-sb;
			if(ans<mx||!ck(ans))puts("-1");
			else printf("%lld\n",(1ll*n*m*ans-sw-sb)/2);
			continue;
		}
		if(sb!=sw){puts("-1");continue;}
		ll l=mx-1,r=5e9;
		while(l+1<r)
		{
			ll mid=(l+r)>>1;
			ck(mid)?r=mid:l=mid;
		}
		printf("%lld\n",(1ll*n*m*r-sw-sb)/2);
	}
	return 0;
}

80人環遊世界

solution

考慮將每個國家拆點,分為入點和出點,之間連線容量為\(v_i\) 的邊。

但是這樣做並不能保證每個國家恰好經過\(v_i\) 次,於是將其費用令為\(-\infty\) ,這樣就能誘導它把容量流完。

然後對於從\(i\)\(j\) 的機票價值為\(w\) ,我們連線\(i\) 的出點和\(j\) 的入點,容量為\(+\infty\) ,費用為\(w\)

建一個點向所有入點連邊,容量\(+\infty\) ,費用為\(0\) ,代表開始可以從任意位置出發。源點向這個點連容量為\(m\) 的邊。

所有出點向匯點連邊,容量\(+\infty\) ,費用為\(0\) ,代表開始可以從任意位置結束。

假設最後最小費用最大流跑出來的答案為\(ans\) ,那麼最終答案為\(ans+sum\times \infty\) ,其中\(sum=\sum v_i\)

code

view code
int n,m,x,sum;
int main()
{
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		scanf("%d",&x),add(i,i+n,x,-inf),sum+=x;
	nn=n*2;
	for(int i=1;i<n;++i)
		for(int j=i+1;j<=n;++j)
		{
			scanf("%d",&x);
			if(x<0)continue;
			add(i+n,j,_inf,x);
		}
	S=++nn;
	for(int i=1;i<=n;++i)add(S,i,_inf,0);
	add(++nn,S,m,0);S=nn;
	T=++nn;
	for(int i=1;i<=n;++i)add(i+n,T,_inf,0);
	printf("%lld\n",dinic().second+inf*sum);
	return 0;
}

[SHOI2003]吃豆豆

solution

首先路徑不能相交沒有任何用處,因為如果相交了,那麼在相交點兩人分別掉頭即可,不會影響最終答案。

有一種顯然的建圖方式:將每個豆豆拆點,由入點向出點連兩條邊,一條流量為\(1\) 費用為\(1\) ,另一條流量為\(+\infty\) ,費用為\(0\) ,代表這個點可以經過多次但只有一次會計算貢獻。然後合法的點對間出點連向入點即可。最後跑最大費用最大流。

直接這樣會超時,因為連的邊太多。考慮優化。注意到如果\(i\rightarrow j,j\rightarrow k\) ,那麼\(i\rightarrow k\) 這條邊是沒有必要的。於是可以排一遍序然後隨便搞一波就行。(這裡並沒有最小化連的邊的數量,只是比暴力連邊稍微好了些,應該還是可能會被卡

code

view code
struct pt{int x,y;}p[P];
inline bool operator<(const pt&x,const pt&y){return x.x!=y.x?x.x<y.x:x.y<y.y;}
int n;
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)scanf("%d%d",&p[i].x,&p[i].y);
	sort(p+1,p+n+1);
	nn=n*2;
	for(int i=1;i<=n;++i)add(i,i+n,1,1),add(i,i+n,inf,0);
	S=++nn;
	for(int i=1;i<=n;++i)add(S,i,inf,0);
	add(++nn,S,2,0);S=nn;
	T=++nn;
	for(int i=1;i<=n;++i)add(i+n,T,inf,0);
	for(int i=1;i<=n;++i)
	{
		int now=inf;
		for(int j=1;j<=n;++j)
		{
			if(i==j||p[j].x<p[i].x||p[j].y<p[i].y)continue;
			if(now>=p[j].y)add(i+n,j,inf,0),now=p[j].y;
		}
	}
	printf("%d\n",dinic().second);
	return 0;
}
NO PAIN NO GAIN