网络流24题部分题解

前言

最近在刷网络流……可能\(NOIP\)用不到但是自己感觉多会点东西总是好的,既然学了网络流就多见几个模型。这里记录超级简要的思路和主要代码。

1.飞行员配对

二分图大水题,没啥可说的,匈牙利\(Dinic\)都能搞。

2.太空飞行计划问题

显然最大权闭合子图问题,源点向每个实验连一条流量为收益的边,实验向需要的器材连流量为无穷的边,器材向原点连流量为代价的边,最后用总收益减去最大流。

输出方案卡了半天。思考一下实际意义,可以发现器材和实验都选和源点联通的,即\(dep\)不为\(0\)的。

int main(){
	tot=1;
	scanf("%d%d",&m,&n);
	for(re int i=1,w;i<=m;i++){
		scanf("%d",&w),add(s,i,w),sum+=w;
		char tools[10000];//Windows换行符\r\n读入法
		memset(tools,0,sizeof tools);
		cin.getline(tools,10000);
		int ulen=0,tool;
		while (sscanf(tools+ulen,"%d",&tool)==1){
    		vec[i].push_back(tool),add(i,tool+m,inf);
    		if(tool==0) ulen++;
    		else while (tool) tool/=10,ulen++;
		    ulen++;
		}
	}
	for(re int i=1,w;i<=n;i++) scanf("%d",&w),add(i+m,t,w);
	int tmp=dinic();
	for(re int i=1;i<=m;i++) if(dep[i]>0) printf("%d ",i);
	printf("\n");
	for(re int i=1;i<=n;i++) if(dep[i+m]>0) printf("%d ",i);
	printf("\n");
	printf("%d\n",sum-tmp);
	return 0;
}

3.试题库问题

建图挺套路的。分成两边,试题一边,分类一边。试题向可以属于的分类连边(流量为\(1\)代表每个题只能用一次),原点向每个题连边(流量为\(1\)),每个类向汇点连边(流量为\(w\)代表每个类需要\(w\)题),跑最大流,如果最大流凑不够\(m\)则无解。

输出方案遍历试题和分类的连边,边权为\(0\)则代表被选中,即本分类选中了本题。

4.最小路径覆盖问题

居然有人会把建图方法写在题面上……

把每个点拆成两个,源点连向所有入点,所有出点连向汇点,有边则从起点的入点连向终点的出点,流量均为\(1\),跑最大流即可。

输出方案时还是遍历每个边,边权变为\(0\)说明这条边被选中了,记录一下找到起点挨个递归输出即可。

void find(int u){
	if(!u) return printf("\n"),void();
	printf("%d ",u),find(to[u]);
}
int main(){
	tot=1;
	scanf("%d%d",&n,&m);
	for(re int i=1;i<=m;i++){
		int x,y;
		scanf("%d%d",&x,&y);
		add(x,y+n,1);
	}
	for(re int i=1;i<=n;i++) add(s,i,1),add(i+n,t,1);
	int ans=n-dinic();
	for(re int i=2;i<=tot;i+=2) if(!e[i].w) vis[e[i].to-n]=1,to[e[i].from]=e[i].to-n;
	for(re int i=1;i<=n;i++) if(!vis[i]) find(i);
	printf("%d\n",ans);
	return 0;
}

5.魔术球问题

贪心可过

#include<bits/stdc++.h>
#define re register
using namespace std;
const int lzw=55+3;
int n,tot;
vector<int> v[lzw];
bool is_sqrt(int x){
	return (int)sqrt(x)*(int)sqrt(x)==x;
}
int main(){
	scanf("%d",&n);
	int num=0;
	while(1){
		num++;
		bool flag=0;
		for(re int i=tot;i>=1;i--){
			if(is_sqrt(num+v[i][v[i].size()-1])){
				v[i].push_back(num),flag=1;
				break;
			}
		}
		if(flag) continue;
		if(tot==n) break;
		v[++tot].push_back(num);
	}
	printf("%d\n",num-1);
	for(re int i=1;i<=n;i++){
		for(re int j=0;j<v[i].size();j++) printf("%d ",v[i][j]);
		printf("\n");
	}
	return 0;
}

6.方格取数问题

网格问题经典黑白染色模型。白格分成一部,黑格分成一部,分别从源点/向汇点连边权为\(w\)的边。

先假设所有的各自都选上,问题转化为最少要去掉几个的最小割问题。相邻的点一定分属黑白两部,所以白向黑连边,最后用总和减去最小割/最大流即可。

int main(){
	tot=1;
	scanf("%d%d",&m,&n);
	for(re int i=1;i<=m;i++)
		for(re int j=1;j<=n;j++)
			id[i][j]=++cnt;
	for(re int i=1;i<=m;i++){
		for(re int j=1;j<=n;j++){
			int w;
			scanf("%d",&w),ans+=w;
			if(i+j&1) add(id[i][j],t,w);
			else{
				add(s,id[i][j],w);
				for(re int k=0;k<4;k++){
					if(tx<1||tx>m||ty<1||ty>n) continue;
					add(id[i][j],id[tx][ty],inf);
				}
			}
		}
	}
	printf("%d\n",ans-dinic());
	return 0;
}

7.圆桌问题

(白嫖\(R\color{red}{value}\)博客)

首先可以看出一个明显的二分图模型, 单位是一种点, 餐桌是另一种点, 代表数量可以看做是流量。

\(s\)连到所有单位点, 容量为代表数量。

单位点和餐桌点之间两两连一条容量为\(1\)的边代表"同一个单位来的代表不能在同一个餐桌就餐"的限制. 餐桌点再向\(t\)连一条容量为餐桌大小的边来限制这个餐桌的人数。

这样的话, 每一单位流量都代表着一个代表. 它们流经的点和边就代表了它们的特征. 而容量就是对代表的限制。

跑最大流即可。

8.骑士共存问题

网格问题黑白染色模型。参照方格取数,别的没啥可说的。

9.分配问题

分成两部,跑费用流,挺水的。跑最大费用的时候把\(SPFA\)改成求最长路即可,注意这时候memset(dis,128,sizeof(dis)),不能设置成\(-1\)

10.运输问题

裸费用流。

11.负载平衡问题

我记得有数学做法来着,好像是这篇博客

网络流做法的话也比较简单,用流量限制转移数量,费用为转移次数。不到平均值的从源点向其连边,否则向汇点连边。可以相互传递的点之间连上流量为无穷,费用为\(1\)的边。

int main(){
	tot=1;
	scanf("%d",&n);
	for(re int i=1;i<=n;i++) scanf("%d",&a[i]),ave+=a[i];
	ave/=n;
	for(re int i=1;i<=n;i++){
		int tmp=a[i]-ave;
		if(tmp>0) add(s,i,tmp,0);
		else add(i,t,-tmp,0);
		if(i==1) add(1,n,inf,1),add(1,2,inf,1);
		else if(i==n) add(n,1,inf,1),add(n,n-1,inf,1);
		else add(i,i+1,inf,1),add(i,i-1,inf,1);
	}
	printf("%d\n",ek());
	return 0;
}

12.航空路线问题

首先应该想到正着从起点到终点走一次再倒着走回来一次可以转化为正着选两条没有相交的路线(起点终点除外),所以不难想到拆点+费用流,用流量限制走几次,费用则是城市带来的贡献,最后输出方案从起点开始\(DFS\)选两条流量均为\(0\)的路线即可。

void dfs(int u){
	if(u!=1&&u!=1+n&&u!=n&&u!=n*2) used[u]=1;
	if(u<=n) stk[++top]=u;
	for(re int i=head[u];i;i=e[i].next){
		if(!(i&1)&&!used[v]&&!e[i].w){
			dfs(v);
			return;
		}
	}
}
int main(){
	tot=1;
	cin>>n>>m;
	for(re int i=1;i<=n;i++){
		cin>>name[i],id[name[i]]=i;
		if(i==1||i==n) add(i,i+n,2,0);
		else add(i,i+n,1,1);
	}
	for(re int i=1;i<=m;i++){
		string xx,yy;
		int x,y;
		cin>>xx>>yy;
		x=id[xx],y=id[yy];
		if(x==y) continue;
		if(x>y) swap(x,y);
		if(x==1&&y==n) check=1;
		add(x+n,y,1,0);
	}
	int ans=ek()+2;
	if(val==2) cout<<ans<<'\n';
	else if(val==1&&check) return puts("2"),cout<<name[1]<<'\n'<<name[n]<<'\n'<<name[1]<<'\n',0;
	else return puts("No Solution!"),0;
	top=0,dfs(1);
	for(re int i=1;i<=top;i++) cout<<name[stk[i]]<<'\n';
	top=0,dfs(1);
	for(re int i=top-1;i>=1;i--) cout<<name[stk[i]]<<'\n';
	return 0;
}

13.数字梯形问题

仍然非常套路的费用流,用流量表示限制,费用表示限制。点的限制直接拆点,有限制流量唯一,否则直接拆点。边的限制同理。

int main(){
	scanf("%d%d",&m,&n);
	for(re int i=1;i<=n;i++)
		for(re int j=1;j<=m+i-1;j++)
			scanf("%d",&a[i][j]),id[i][j]=++cnt;
	tot=1;
	for(re int i=1;i<=m;i++) add(s,id[1][i],1,0);
	for(re int i=1;i<=n-1;i++)
		for(re int j=1;j<=m+i-1;j++)
			add(id[i][j]+cnt,id[i+1][j],1,0),add(id[i][j]+cnt,id[i+1][j+1],1,0);
	for(re int i=1;i<=m+n-1;i++) add(id[n][i]+cnt,t,1,0);
	for(re int i=1;i<=n;i++)
		for(re int j=1;j<=m+i-1;j++)
			add(id[i][j],id[i][j]+cnt,1,a[i][j]);
	printf("%d\n",ek());
	memset(head,0,sizeof(head)),tot=1;
	for(re int i=1;i<=m;i++) add(s,id[1][i],1,0);
	for(re int i=1;i<=n-1;i++)
		for(re int j=1;j<=m+i-1;j++)
			add(id[i][j]+cnt,id[i+1][j],1,0),add(id[i][j]+cnt,id[i+1][j+1],1,0);
	for(re int i=1;i<=m+n-1;i++) add(id[n][i]+cnt,t,inf,0);
	for(re int i=1;i<=n;i++)
		for(re int j=1;j<=m+i-1;j++)
			add(id[i][j],id[i][j]+cnt,inf,a[i][j]);
	printf("%d\n",ek());
	memset(head,0,sizeof(head)),tot=1;
	for(re int i=1;i<=m;i++) add(s,id[1][i],1,0);
	for(re int i=1;i<=n-1;i++)
		for(re int j=1;j<=m+i-1;j++)
			add(id[i][j]+cnt,id[i+1][j],inf,0),add(id[i][j]+cnt,id[i+1][j+1],inf,0);
	for(re int i=1;i<=m+n-1;i++) add(id[n][i]+cnt,t,inf,0);
	for(re int i=1;i<=n;i++)
		for(re int j=1;j<=m+i-1;j++)
			add(id[i][j],id[i][j]+cnt,inf,a[i][j]);
	printf("%d\n",ek());
	return 0;
}

14.火星探险问题

感觉这题思路还是挺好的。

本题主要的不同点显然在于每个有岩石标本的点只能被第一个到达的车拿一次。因为这个点之后还能接着走所以直接拆点把流量设为\(1\)显然不行。

经过一番撕烤我们发现对于有岩石标本的点,如果它的岩石标本已经被拿了那么它和普通点就莫得区别,于是灵光一现,对于这样的点我们好像可以开两个边,一个流量为\(1\)费用为\(1\),另一个和普通点一样流量无穷,费用为\(0\)。然后就可以了!

输出方案直接\(DFS\)即可。

void dfs(int id,int u){
	vis[u]=1;
	if(u==t) return;
	for(re int i=head[u];i;i=e[i].next){
		if(vis[v]||e[i].w==inf) continue;
		if(u>cnt){
			if(v==u-cnt+1) printf("%d 1\n",id);
			else printf("%d 0\n",id);
		}
		e[i].w++,dfs(id,v);
		return;
	}
}
int main(){
	tot=1;
	scanf("%d%d%d",&n,&p,&q);
	for(re int i=1;i<=q;i++)
		for(re int j=1;j<=p;j++)
			scanf("%d",&mp[i][j]),id[i][j]=++cnt;
	for(re int i=1;i<=q;i++){
		for(re int j=1;j<=p;j++){
			if(mp[i][j]==1) continue;
			if(id[i][j]==cnt){
				if(mp[i][j]!=2) add(id[i][j],id[i][j]+cnt,n,0);
				else add(id[i][j],id[i][j]+cnt,n-1,0);
			}
			else add(id[i][j],id[i][j]+cnt,inf,0);
			if(mp[i][j]==2) add(id[i][j],id[i][j]+cnt,1,1);
			for(re int k=0;k<2;k++){
				int tx=i+dx[k],ty=j+dy[k];
				if(tx>q||ty>p||mp[tx][ty]==1) continue;
				else add(id[i][j]+cnt,id[tx][ty],inf,0);
			}
		}
	}
	ek();
	for(re int i=1;i<=n;i++) memset(vis,0,sizeof(vis)),dfs(i,s);
	return 0;
}
签名: 我将轻轻叹息,叙述这一切, 许多许多年以后: 林子里有两条路,我—— 选择了行人稀少的那一条, 它改变了我的一生。
posted @ 2020-11-19 21:28  DarthVictor  阅读(149)  评论(2编辑  收藏  举报
莫挨老子!