网络流24题题解(持续更新中)

网络流24题题解(持续更新中)

前排提示

因为笔者太菜了不会做索引。所以读者可以自行通过 F3 等手段直接通过题目或题目中的关键字查找相应内容。

太空飞行计划

题目链接:https://loj.ac/p/6001

最大权闭合子图模板。

贴个链接:https://www.cnblogs.com/dilthey/p/7565206.html

(以后再补)

代码如下:

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN = 5e3+5;
const ll INF = 1e18;
int m,n;
int dep[MAXN],now[MAXN],Ans[MAXN];
struct E
{
	int to,pre;ll w;
}e[MAXN<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,ll w)
{
	e[++tot_E]={v,head[u],w};
	head[u]=tot_E;
}
bool bfs()
{
	memset(dep,0,sizeof dep);
	queue <int> q;
	q.push(0);dep[0]=1;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to;ll w=e[i].w;
			if(w&&!dep[to])
			{
				dep[to]=dep[p]+1;
				q.push(to);
			}
		}
	}
	return dep[m+n+1];
}
ll dfs(int p,ll in)
{
	if(p==m+n+1) return in;
	ll out=0;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to;ll w=e[i].w;
		if(w&&dep[to]==dep[p]+1)
		{
			ll res=dfs(to,min(in,w));
			e[i].w-=res;
			e[i^1].w+=res;
			out+=res;
			in-=res;
		}
	}
	if(out==0) dep[p]=0;
	return out;
}
int main()
{
	scanf("%d %d",&m,&n);
	ll ans=0;
	for(int i=1;i<=m;++i)
	{
		int u;char str;
		scanf("%d",&u);ans+=u;
		add(0,i,u);add(i,0,0);
		while(scanf("%d%c",&u,&str)!=EOF)
		{
			add(i,u+m,INF);
			add(u+m,i,0);
			if(str=='\n'||str=='\r') break;
		}
	}
	for(int i=1;i<=n;++i)
	{
		int c;scanf("%d",&c);
		add(i+m,m+n+1,c);
		add(m+n+1,i+m,0);
	}
	
	while(bfs()) ans-=dfs(0,INF);
	for(int i=1;i<=m;++i)
		if(dep[i]) Ans[++Ans[0]]=i;
	for(int i=1;i<Ans[0];++i) printf("%d ",Ans[i]);
	printf("%d\n",Ans[Ans[0]]);
	Ans[0]=0;
	for(int i=m+1;i<=n+m;++i)
		if(dep[i]) Ans[++Ans[0]]=i-m;
	for(int i=1;i<Ans[0];++i) printf("%d ",Ans[i]);
	printf("%d\n",Ans[Ans[0]]);
	printf("%lld\n",ans);
	return 0;
}

最小路径覆盖

链接:https://loj.ac/p/6002

注意到一条路径除了头尾,位于路径中间的点的入度与出度恰好\(1\)。因此,我们可以将一个点拆为两个点,一个点表示出度,一个点表示入度,分别位于二分图的左部和右部。

题目给的边 \(u,v\) 相当于 \(u\) 的出度点连向 \(v\) 的入度点,由于每个点的出入度恰好为 \(1\)。在二分图的体现中就是左部中的点与右部中点的匹配。每个左部点只连向一个右部点,即路径中一个点连向一个点的后继,每个右部点只被一个左部点连,相当于路径中的一个点与这个点的前驱相连。而路径的头,尾比较特殊,可能只有后继,可能只有前驱,也可能前驱后继都没有(路径中只有一个点的情况)。这在二分图中是由出度点/入度点没有对应与之匹配的点体现出来的。

所以一个图的路径覆盖集的大小就是一张二分图的总点数 \(-\) 匹配点数。

那么我们要路径覆盖集大小最小,就得让匹配点数最大。

所以用网络流/匈牙利算法求出最大匹配即可。

输出方案可以枚举左部点,如果左部点未输出过,就迭代的遍历它的匹配点,它匹配点对应左部点的匹配点...

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 6e3+5;
const int INF = 0x3f3f3f3f;
struct E
{
	int to,pre,w;
}e[MAXN<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,int w)
{
	e[++tot_E]=E{v,head[u],w};
	head[u]=tot_E;
}
int n,m;
int dep[MAXN],now[MAXN],mat[MAXN];
bool vis[MAXN];
bool bfs()
{
	queue <int> q;
	memset(dep,0,sizeof dep);
	q.push(0);dep[0]=1;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w;
			if(w&&!dep[to])
			{
				dep[to]=dep[p]+1;
				q.push(to);
			}
		}
	}
	return dep[2*n+1];
}
int dfs(int p,int in)
{
	if(p==2*n+1) return in;
	int out=0;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to,w=e[i].w;
		if(w&&dep[p]+1==dep[to])
		{
			int res=dfs(to,min(w,in));
			e[i].w-=res;
			e[i^1].w+=res;
			out+=res;
			in-=res;
		}
	}
	if(out==0) dep[p]=0;
	return out;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=m;++i)
	{
		int u,v;
		scanf("%d %d",&u,&v);
		add(u,v+n,1);add(v+n,u,0);
	}
	for(int i=1;i<=n;++i)
	{
		add(0,i,1);add(i,0,0);
		add(i+n,2*n+1,1);add(2*n+1,n+i,0);
	}
	int ans=n;
	while(bfs()) ans-=dfs(0,INF);
	for(int i=1;i<=n;++i)
	{
		for(int j=head[i];j;j=e[j].pre)
		{
			int to=e[j].to,w=e[j].w;
			if(w==0&&to>=n+1&&to<=2*n)
			{
				mat[i]=to;
				break;
			}
		}
	}
	for(int i=1;i<=n;++i)
	{
		if(vis[i]) continue;
		int x=i+n;
		while(x)
		{
			x-=n;
			vis[x]=1;
			printf("%d ",x);
			x=mat[x];
		}
		printf("\n");
	}
	printf("%d\n",ans);
	return 0;
}

圆桌聚餐

链接:https://loj.ac/p/6004

每张桌子的容量是有限制的,每个代表团到每张桌子的人数也是有限制的。

那么我们可以想到网络流的一个性质:对于一个节点,它输出的流量与输入的流量是相等的。

以此,我们进行构图。

首先构建一个超级源点,将每一个代表团抽象成点,超级源点向每一个代表团连一条单向边,边权为代表团的人数

再将每张桌子抽象成点,每个代表团向所有桌子都连一条权值为 \(1\) 的单向边。

再建立超级汇点。将每个桌子与超级汇点连一条单向边,边权为桌子的容量。

代表团向桌子输出流,相当于就是派人到那张桌子,而边权为 \(1\) 则保证了每个代表团只会向一张桌子派一个人。

而超级源点与超级汇点分别于代表团桌子连边,就是利用了上面那个性质,让他们输出与输入的流量相同。这保证每个代表团只会输出那么多人,也保证每个桌子只会接受那么多输入。

然后我们跑一遍最大流,如果最大流与代表数总和相等则说明存在方案。否则不存在。

输出方案详见代码。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXE = 2e5+5;
const int INF = 0x3f3f3f3f;
const int MAXN = 1e4+5;
struct E
{
	int to,pre,w;
}e[MAXE<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,int w)
{
	e[++tot_E]=E{v,head[u],w};
	head[u]=tot_E;
}
int now[MAXN],dep[MAXN],n,m;
bool bfs()
{
	memset(dep,0,sizeof dep);
	queue <int> q;q.push(0);dep[0]=1;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w;
			if(w&&!dep[to])
			{
				dep[to]=dep[p]+1;
				q.push(to);
			}
		}
	}
	return dep[n+m+1];
}
int dfs(int p,int in)
{
	if(p==n+m+1) return in;
	int out=0;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to,w=e[i].w;
		if(w&&dep[to]==dep[p]+1)
		{
			int res=dfs(to,min(in,w));
			e[i].w-=res;
			e[i^1].w+=res;
			out+=res;
			in-=res;
		}
	}
	if(!out) dep[p]=0;
	return out;
}
int main()
{
	scanf("%d %d",&m,&n);
	for(int i=1;i<=m;++i)
	{
		for(int j=1;j<=n;++j)
		{
			add(i,j+m,1);
			add(j+m,i,0);
		}
	}
	int ans=0;
	for(int i=1;i<=m;++i)
	{
		int x;scanf("%d",&x);ans+=x;
		add(0,i,x);add(i,0,0);
	}
	for(int i=1;i<=n;++i)
	{
		int x;scanf("%d",&x);
		add(i+m,n+m+1,x);add(n+m+1,i+m,0);
	}
	while(bfs()) ans-=dfs(0,INF);
	if(ans==0)
	{
		printf("1\n");
		for(int i=1;i<=m;++i)
		{
			for(int j=head[i];j;j=e[j].pre)
			{
				int to=e[j].to,w=e[j].w;
				if(!w&&to>=m+1&&to<=m+n)
					printf("%d ",to-m);
			}
			printf("\n");
		}
	}
	else printf("0\n");
	return 0;
}

最长递增子序列

链接:https://loj.ac/p/6005

这题题意有些问题,最好去luogu上看看:https://www.luogu.com.cn/problem/P2766

luogu上的题意是修过的。

以下递增皆为非严格递增

第一个问题我们可以用 dp 来解决。我们设 \(f_i\) 表示以 \(i\) 为结尾的最长递增子序列的长度。然后 \(O(n^2)\) 处理即可。

第二问,相当于一个数只能出现在一个序列中,同样是只有一个是它的前驱,一个是它的后继。我们可以利用相同的技巧,将一个点分为入度点和出度点,所有点的出度点构成左部点,所有点的入度点构成右部点。

然后考虑连边的方式,一个点是另一个点的后继,在原题目中的表现形式是 \(f_j=f_i+1\)\(x_i\le x_j\)

\(x_i\le x_j\) 好理解,为什么 \(f_j=f_i+1\) 呢?这是因为如果 \(f_j\not=f_i+1\) 说明 \(f_j\) 不能从 \(f_i\) 转移而来的,它是由其他点转移过来的。而且 \(j\)\(i\) 的后继时,这个子序列的长度不可能是最长的。

建完图后,我们不能直接利用 \(n\) \(-\) 最大匹配数求出第二问的答案,因为并不是每一个点都能当做起点的

所以我们得换一种形式,建立超级源点,然后只向 \(f_i=1\) 的点连一条权值为 \(1\) 的边。同理,建立超级汇点,所有 \(f_i=len\)\(len\) 为第一问答案)的点向超级汇点连一条权值为 \(1\) 的边 。此外,我们还要从每个点的入度点向出度点连一条权值为 \(1\) 的边。入度点向出度点连边是为了让一个点接受到的流能继续传递下去从而汇到超级汇点(因为这题并不是每个点都是起点,也并不是每个点都是终点

那么图就建好了,我们跑一边最大流就是答案。

第三问题意有些问题,建议先去 luogu 上看了。然后我们会发现我们只要将 \(1\) 与超级源点和超级汇点之间的边(假如存在的话)的边权改为 \(INF\) 。同理将 \(n\) 与超级源点和超级汇点之间的边(假如存在的话)的边权改为 \(INF\)。然后跑一遍最大流即可。

这里切记 \(1\) 的出度点和 \(n\) 的入度点与其他出入度点的边权仍然是 \(1\)。不然就会求出重复的递增子序列(重复这个概念请去 luogu 看题意)

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXE = 1e5+5;
const int MAXN = 1e3+5;
const int INF = 0x3f3f3f3f;
int n,A[MAXN],f[MAXN];
struct E
{
	int to,pre,w;
}e[MAXE<<1];
int head[MAXN],tot_E;
void add(int u,int v,int w)
{
	e[++tot_E]=E{v,head[u],w};
	head[u]=tot_E;
}
int len,dep[MAXN],now[MAXN];
bool bfs()
{
	memset(dep,0,sizeof dep);
	queue <int> q;
	q.push(0);dep[0]=1;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w;
			if(w&&!dep[to])
			{
				dep[to]=dep[p]+1;
				q.push(to);
			}
		}
	}
	return dep[2*n+1];
}
int dfs(int p,int in)
{
	if(p==2*n+1) return in;
	int out=0;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		int to=e[i].to,w=e[i].w;
		if(w&&dep[to]==dep[p]+1)
		{
			int res=dfs(to,min(in,w));
			out+=res;
			in-=res;
			e[i].w-=res;
			e[i^1].w+=res;
		}
	}
	if(out==0) dep[p]=0;
	return out;
}
void init(bool opt)
{
	memset(head,0,sizeof head);tot_E=1;
	for(int i=1;i<=n;++i)
	{
		f[i]=1;
		for(int j=1;j<i;++j)
			if(A[j]<=A[i]) f[i]=max(f[j]+1,f[i]);
		len=max(len,f[i]);
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<i;++j)
		{
			if(A[j]<=A[i]&&f[i]==f[j]+1)
			{
				add(j,n+i,1);
				add(i+n,j,0);
			}
		}
	}
	for(int i=1;i<=n;++i)
	{
		if(len==1)
		{
			add(i,i+n,1);
			add(i+n,i,0);
		}
		add(i+n,i,1);
		add(i,i+n,0);
	}
	for(int i=1;i<=n;++i)
	{
		int w=1;
		if(opt&&(i==1||i==n)) w=INF;
		if(f[i]==1)
		{
			add(0,i,w);
			add(i,0,0);
		}
		if(f[i]==len)
		{
			add(i+n,2*n+1,w);
			add(2*n+1,i+n,0);
		}
	}
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		scanf("%d",&A[i]);
	init(0);
	printf("%d\n",len);
	int ans=0;
	while(bfs()) ans+=dfs(0,INF);
	printf("%d\n",ans);
	ans=0;
	init(1);
	while(bfs()) ans+=dfs(0,INF);
	printf("%d\n",ans);
	return 0;
}

试题库

链接:https://loj.ac/p/6006

与圆桌聚餐一样,利用一个点的输出量与输入量一致

每道题能对一个类别做出贡献。所以超级源点向每道题建一条权值为 \(1\) 的边。每道题向它属于的类别连一条权值为 \(1\) 的边。

每种类别的题目数一定,所以每个类别向超级汇点建立一条权值为需要选出题目数的边,来保证每个种类最多只能产生那么多贡献。

然后跑一遍最大流,如果答案与 \(m\) 相等,则有解,否则无解。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e3+5;
const int MAXE = 1e5+5;
const int INF = 0x3f3f3f3f;
int n,K,m;
struct E
{
	int to,pre,w;
}e[MAXE<<1];
int tot_E=1,head[MAXN];
void add(int u,int v,int w)
{
	e[++tot_E]=E{v,head[u],w};
	head[u]=tot_E;
}
int dep[MAXN],now[MAXN];
bool bfs()
{
	memset(dep,0,sizeof dep);
	queue <int> q;
	q.push(0);dep[0]=1;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w;
			if(w&&!dep[to])
			{
				dep[to]=dep[p]+1;
				q.push(to);
			}
		}
	}
	return dep[n+K+1];
}
int dfs(int p,int in)
{
	if(p==n+K+1) return in;
	int out=0;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to,w=e[i].w;
		if(w&&dep[to]==dep[p]+1)
		{
			int res=dfs(to,min(in,w));
			e[i].w-=res;
			e[i^1].w+=res;
			in-=res;
			out+=res;
		}
	}
	if(out==0) dep[p]=0;
	return out;
}
int main()
{
	scanf("%d %d",&K,&n);
	for(int i=1;i<=K;++i)
	{
		int x;scanf("%d",&x);
		m+=x;
		add(n+i,n+K+1,x);add(n+K+1,n+i,0);
	}
	for(int i=1;i<=n;++i)
	{
		int p;scanf("%d",&p);
		while(p--)
		{
			int x;scanf("%d",&x);
			add(i,x+n,1);add(x+n,i,0);
		}
	}
	for(int i=1;i<=n;++i)
	{
		add(0,i,1);
		add(i,0,1);
	}
	int ans=m;
	while(bfs()) ans-=dfs(0,INF);
	if(!ans)
	{
		for(int i=1+n;i<=n+K;++i)
		{
			printf("%d: ",i-n);
			for(int j=head[i];j;j=e[j].pre)
			{
				int to=e[j].to,w=e[j].w;
				if(w==1&&to>=1&&to<=n) printf("%d ",to);
			}
			printf("\n");
		}
	}
	else printf("No Solution!");
	return 0;
}

方格取数

题目链接:https://loj.ac/p/6007

原问题可以转化为取出若干数,使取出后方格内任意两个数所在方格没有公共边,且让剩下的数总和最大。

我们将相邻的点连上边,可以发现,这幅图构成一幅二分图(\((1,1)(1,3)\dots(2,2)(2,4)\dots\) 这些点之间不会有边),且每一个点有一个权值,我们要做的去掉某些点使得原图中不再存在任何边,且剩余点的权值最大

我们可以用网络流来解决这个模型。

我们发现,当原图不存在任何边时,左部的点和右部的点是不连通的。我们建立超级源点,左部的点都与超级源点右边,右部的点都与超级汇点有边。那么在网络中的体现就是超级源点与超级汇点在两个联通块内。容易发现我们要求的就是原图的割,我们想要剩下来的点权最大就是要求最小割。

那么我们将超级源点与所有左部的点连一条权值为点权的边,所有右部的点与超级汇点连一条权值为 点权的边,然后将原矩阵中相邻的点连边,方向从左部到右部,权值为 \(INF\) 表示最小割不应该将这条边算进

实质上便是将点化成了与超级源点/汇点相连的边,删掉一条边就是不选择对应的那个点。

然后我们跑出最小割就行。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 1e4+5;
const int MAXE = 2e4+5;
const int INF = 0x3f3f3f3f;
struct E
{
	int to,pre,w;
}e[MAXE<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,int w)
{
	e[++tot_E]=E{v,head[u],w};
	head[u]=tot_E;
}
int n,m,A[MAXN][MAXN],dep[MAXN],now[MAXN];
int fx[4]={1,-1,0,0};
int fy[4]={0,0,1,-1};
int id(int x,int y)
{
	return (x-1)*m+y;
}
bool check(int x,int y)
{
	if(x>=1&&x<=n&&y>=1&&y<=m) return true;
	return false;
}
bool bfs()
{
	queue <int> q;
	memset(dep,0,sizeof dep);
	q.push(0);dep[0]=1;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w;
			if(w&&!dep[to])
			{
				dep[to]=dep[p]+1;
				q.push(to);
			}
		}
	}
	return dep[n*m+1];
}
int dfs(int p,int in)
{
	if(p==n*m+1) return in;
	int out=0;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to,w=e[i].w;
		if(w&&dep[to]==dep[p]+1)
		{
			int res=dfs(to,min(in,w));
			in-=res;out+=res;
			e[i].w-=res;e[i^1].w+=res;
		}
	}
	if(out==0) dep[p]=0;
	return out;
}
int main()
{
	scanf("%d %d",&n,&m);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=m;++j)
			scanf("%d",&A[i][j]);
	int opt=1;
	int ans=0;
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=m;++j)
		{
			ans+=A[i][j];
			if(opt)
			{
				add(0,id(i,j),A[i][j]);
				add(id(i,j),0,0);
				for(int k=0;k<4;++k)
				{
					int xx=i+fx[k],yy=j+fy[k];
					if(check(xx,yy))
					{
						add(id(i,j),id(xx,yy),INF);
						add(id(xx,yy),id(i,j),0);
					}
				}
			}
			else
			{
				add(id(i,j),n*m+1,A[i][j]);
				add(n*m+1,id(i,j),0);
			}
			opt^=1;
		}
		if(m%2==0) opt^=1;
	}
	while(bfs()) ans-=dfs(0,INF);
	printf("%d\n",ans);
	return 0;
}

餐巾计划

链接:https://loj.ac/p/6008

很容易看出这是一道费用流问题。

我们对于每天建立一个节点,并且用每个节点向超级汇点连一条容量为 \(r_i\),花费为 \(0\) 的边。这样保证每个节点都能接收到 \(r_i\) 的输入。同时用超级源点向每个节点连一条容量为 \(INF\) 花费为 \(P\) 的边,从这类边输出的流相当于是买的新餐巾

然后我们考虑建边来模拟洗餐巾的操作。由于每天送去洗的餐巾数是一定的。也就是每天送到慢洗部和快洗部的餐巾数量总和有上限但单独送到某一个部的数量无上限。所以我们考虑对每一天再建立一个虚拟节点,表示送去洗的餐巾流向。

那么我们用超级源点向每个虚拟节点连一条容量为 \(r_i\),花费为 \(0\) 的边即可。

那么送到快洗部就相当于是从虚拟节点向时间大于 \(i+M\) 的节点连一条容量为 \(INF\),花费为 \(F\) 的边,同理可以建出送到慢洗部的边。

但是这样我们的边数是 \(O(n^2)\) 的,最坏情况下达到了 \(10^6\) 这个数量级,直接跑会T,我们考虑化简边。

我们发现 \(i\) 连向 \(i+M+x\)\(x\) 为未知量)一条容量为 \(INF\) 花费为 \(C\) 的边等价于一条从 \(i\)\(i+x\) 容量为 \(INF\),花费为 \(0\) 的边加上一条从 \(i+x\)\(i+M+x\) 容量为 \(INF\),花费为 \(C\) 的边。

所以,我们将每个虚拟节点 \(i\) 向虚拟节点 \(i+1\) 连一条容量为 \(INF\) 花费为 \(0\) 的边,而且向 \(i+M\)\(i+N\) 分别建容量为 \(INF\) 花费为 \(F/S\) 的边。不难发现,这样构建的网络与我们一开始建立的网络是等价的。

然后跑费用流即可。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 2e3+5;
const int MAXE = 1e6+5;
const int INF = 0x3f3f3f3f;
struct E
{
	int to,pre,w,c;
}e[MAXE<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,int w,int c)
{
	e[++tot_E]={v,head[u],w,c};
	head[u]=tot_E;
}
bool vis[MAXN];
int dis[MAXN],now[MAXN];
int n,P,M,F,N,S,ans;
bool spfa()
{
	memset(dis,0x3f,sizeof dis);
	memset(vis,0,sizeof vis);
	queue <int> q;q.push(0);dis[0]=0;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		now[p]=head[p];
		vis[p]=0;
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w,c=e[i].c;
			if(w&&dis[to]>dis[p]+c)
			{
				dis[to]=dis[p]+c;
				if(!vis[to])
				{
					q.push(to);
					vis[to]=1;
				}
			}
		}
	}
	memset(vis,0,sizeof vis);
	return dis[2*n+1]<INF;
}
int dfs(int p,int in)
{
	if(p==2*n+1) return in;
	int out=0;
	vis[p]=1;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		int to=e[i].to,w=e[i].w,c=e[i].c;
		now[p]=i;
		if(!vis[to]&&dis[to]==dis[p]+c&&w)
		{
			int res=dfs(to,min(w,in));
			e[i].w-=res;e[i^1].w+=res;
			in-=res;out+=res;
			ans+=res*c;
		}
	}
	vis[p]=0;
	if(out==0) dis[p]=-1;
	return out;
}
int main()
{
	scanf("%d %d %d %d %d %d",&n,&P,&M,&F,&N,&S);
	for(int i=1;i<=n;++i)
	{
		int x;scanf("%d",&x);
		add(0,i,INF,P);add(i,0,0,-P);
		add(i,2*n+1,x,0);add(2*n+1,i,0,0);
		add(0,n+i,x,0);add(n+i,0,0,0);
		if(i+M<=n)
		{
			add(n+i,i+M,INF,F);
			add(i+M,n+i,0,-F);
		}
		if(i+N<=n)
		{
			add(n+i,i+N,INF,S);
			add(i+N,n+i,0,-S);
		}
		if(i<n)
		{
			add(n+i,n+i+1,INF,0);
			add(n+i+1,n+i,0,0);
		}
	}
	int maxflow=0;
	while(spfa()) maxflow+=dfs(0,INF);
	printf("%d\n",ans);
	return 0;
}

分配问题

链接:https://loj.ac/p/6012

一个二分图最大权值匹配的模板。同样的这个问题我们也可以用网络流来解决。

只需要把网络流求二分图最大匹配的板子改一下,改成跑费用流即可。

建出二分图后跑一遍费用流求出最小总效益,将整张图边权取反后再跑一遍费用流,再将答案取反就是最大总效益。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 205;
const int MAXE = 4e4+5;
const int INF = 0x3f3f3f3f;
int n,m;
struct E
{
	int to,pre,w,c;
}e[MAXE<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,int w,int c)
{
	e[++tot_E]=E{v,head[u],w,c};
	head[u]=tot_E;
}
int A[MAXN],B[MAXN],C[MAXN][MAXN],ans,dis[MAXN],now[MAXN];
bool vis[MAXN];
void init(int w)
{
	memset(head,0,sizeof head);tot_E=1;
	for(int i=1;i<=n;++i)
	{
		add(0,i,1,0);
		add(i,0,0,0);
	}
	for(int i=1;i<=n;++i)
	{
		add(n+i,n+n+1,1,0);
		add(n+n+1,n+i,0,0);
	}
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<=n;++j)
		{
			add(i,j+n,1,w*C[i][j]);
			add(j+n,i,0,-w*C[i][j]);
		}
	}
}
bool spfa()
{
	memset(vis,0,sizeof vis);
	memset(dis,0x3f,sizeof dis);
	dis[0]=0;queue <int> q;q.push(0);
	while(!q.empty())
	{
		int p=q.front();
		q.pop();vis[p]=0;now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w,c=e[i].c;
			if(w&&dis[to]>dis[p]+c)
			{
				dis[to]=dis[p]+c;
				if(!vis[to])
				{
					vis[to]=1;
					q.push(to);
				}
			}
		}
	}
	memset(vis,0,sizeof vis);
	return dis[n+n+1]<INF;
}
int dfs(int p,int in)
{
	if(p==n+n+1) return in;
	int out=0;
	vis[p]=1;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to,w=e[i].w,c=e[i].c;
		if(dis[to]==dis[p]+c&&w&&!vis[to])
		{
			int res=dfs(to,min(in,w));
			in-=res;out+=res;
			e[i].w-=res;e[i^1].w+=res;
			ans+=res*c;
		}
	}
	vis[p]=0;
	return out;
}
int main()
{
	scanf("%d",&n);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j)
			scanf("%d",&C[i][j]);
	init(1);
	while(spfa()) dfs(0,INF);
	printf("%d\n",ans);
	ans=0;init(-1);
	while(spfa()) dfs(0,INF);
	printf("%d\n",-ans);
	return 0;
}

负载平衡

链接:https://loj.ac/p/6013

我们设 \(A_i\) 为每个仓库一开始的仓存量。\(cnt=\dfrac{\sum A_i}{n}\)。那么也就是每个仓库最后都只能有 \(cnt\) 件货物,在满足这个条件的情况下,我们要让搬运量最小。而这个目标是与费用流契合的,费用流需要保证最大流量的前提下让消耗最小。所以我们在建模时应该就将存储量抽象为流量,搬运量抽象为边权。

所以我们可以考虑这么建图。建立超级源点,向每个仓库连一条流量为 \(A_i\) 边权为 \(0\) 的边。建立超级汇点,每个仓库向其连一条流量为 \(cnt\) 边权为 \(0\) 的边。这是为了保证每个仓库一开始有 \(A_i\) 的流量,且最后只有 \(cnt\) 的流量流向终点。

然后我们给相邻的仓库间连一条容量为 \(INF\) 边权为 \(1\) 的边,用来搬运货物。

那么跑一遍费用流就是答案了。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 105;
const int MAXE = 1e4+5;
const int INF = 0x3f3f3f3f;
int n,A[MAXN];
struct E
{
	int to,pre,w,c;
}e[MAXE<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,int w,int c)
{
	e[++tot_E]=E{v,head[u],w,c};head[u]=tot_E;
	e[++tot_E]=E{u,head[v],0,-c};head[v]=tot_E;
}
int ans=0;
int dis[MAXN],now[MAXN];
bool vis[MAXN];
bool spfa()
{
	memset(dis,0x3f,sizeof dis);memset(vis,0,sizeof vis);
	queue <int> q;dis[0]=0;q.push(0);
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		vis[p]=0;now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w,c=e[i].c;
			if(w&&dis[to]>dis[p]+c)
			{
				dis[to]=dis[p]+c;
				if(!vis[to])
				{
					q.push(to);
					vis[to]=1;
				}
			}
		}
	}
	memset(vis,0,sizeof vis);
	return dis[n+1]<INF;
}
int dfs(int p,int in)
{
	if(p==n+1) return in;
	int out=0;
	vis[p]=1;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to,w=e[i].w,c=e[i].c;
		if(!vis[to]&&w&&dis[to]==dis[p]+c)
		{
			int res=dfs(to,min(in,w));
			out+=res;in-=res;
			e[i].w-=res;e[i^1].w+=res;
			ans+=res*c;
		}
	}
	vis[p]=0;
	return out;
}
int main()
{
	scanf("%d",&n);
	int sum=0;
	for(int i=1;i<=n;++i) scanf("%d",&A[i]),sum+=A[i];
	sum/=n;
	for(int i=1;i<=n;++i)
	{
		add(0,i,A[i],0);
		add(i,n+1,sum,0);
	}
	for(int i=1;i<=n;++i)
	{
		int nxt=i+1-(i==n)*n,pre=i-1+(i==1)*n;
		add(i,nxt,INF,1);
		add(i,pre,INF,1);
	}
	while(spfa()) dfs(0,INF);
	printf("%d\n",ans);
	return 0;
}

深海机器人问题

链接:https://loj.ac/p/6224

很容易想到按照题意连边,建立超级源点,向每个起点连一条容量为机器人个数,边权为 \(0\) 的边。建立超级汇点,每个终点向其连一条容量为可容纳机器人个数,边权为 \(0\) 的边。

对于题目所给相连的边,由于一条边的价值只有在第一次遍历的时候可以计入贡献,所以对于原题中所给的从 \(u\) 连向 \(v\) 边权为 \(w\) 的边。我们建立一条从 \(u\)\(v\) 容量为 \(1\),边权为 \(w\) 的边。但是因为这条边后面的机器人也能走,所以再建立一条从 \(u\)\(v\) 容量为 \(INF\) 边权为 \(0\) 的边。

将原图边权取反,然后跑一遍费用流再将答案取反即可。

代码如下:

#include<bits/stdc++.h>
using namespace std;
const int MAXN = 305;
const int MAXE = 1e5+5;
const int INF = 0x3f3f3f3f;
int n,m,a,b;
struct E
{
	int to,pre,w,c;
}e[MAXE<<1];
int head[MAXN],tot_E=1;
void add(int u,int v,int w,int c)
{
	e[++tot_E]=E{v,head[u],w,c};head[u]=tot_E;
	e[++tot_E]=E{u,head[v],0,-c};head[v]=tot_E;
}
int id(int x,int y)
{
	return (x-1)*m+y;
}
int now[MAXN],dis[MAXN];
bool vis[MAXN];
bool spfa()
{
	queue <int> q;
	memset(dis,0x3f,sizeof dis);memset(vis,0,sizeof vis);
	q.push(0);dis[0]=0;
	while(!q.empty())
	{
		int p=q.front();
		q.pop();
		vis[p]=0;now[p]=head[p];
		for(int i=head[p];i;i=e[i].pre)
		{
			int to=e[i].to,w=e[i].w,c=e[i].c;
			if(w&&dis[to]>dis[p]+c)
			{
				dis[to]=dis[p]+c;
				if(!vis[to])
				{
					vis[to]=1;
					q.push(to);
				}
			}
		}
	}
	memset(vis,0,sizeof vis);
	return dis[n*m+1]<INF;
}
int ans,maxflow;
int dfs(int p,int in)
{
	if(p==n*m+1) return in;
	int out=0;
	vis[p]=1;
	for(int i=now[p];i&&in;i=e[i].pre)
	{
		now[p]=i;
		int to=e[i].to,w=e[i].w,c=e[i].c;
		if(!vis[to]&&w&&dis[to]==dis[p]+c)
		{
			int res=dfs(to,min(w,in));
			e[i].w-=res;e[i^1].w+=res;
			in-=res;out+=res;
			ans+=res*c;
		}
	}
	vis[p]=0;
	return out;
}
int main()
{
	scanf("%d %d %d %d",&a,&b,&n,&m);++n;++m;
	for(int i=1;i<=n;++i)
	{
		for(int j=1;j<m;++j)
		{
			int w;scanf("%d",&w);
			add(id(i,j),id(i,j+1),1,-w);
			add(id(i,j),id(i,j+1),INF,0);
		}
	}
	for(int i=1;i<=m;++i)
	{
		for(int j=1;j<n;++j)
		{
			int w;scanf("%d",&w);
			add(id(j,i),id(j+1,i),1,-w);
			add(id(j,i),id(j+1,i),INF,0);
		}
	}
	for(int i=1;i<=a;++i)
	{
		int k,x,y;scanf("%d %d %d",&k,&x,&y);
		++x,++y;
		add(0,id(x,y),k,0);
	}
	for(int i=1;i<=b;++i)
	{
		int r,x,y;scanf("%d %d %d",&r,&x,&y);
		++x;++y;
		add(id(x,y),n*m+1,r,0);
	}
	while(spfa()) maxflow+=dfs(0,INF);
	printf("%d\n",-ans);
	return 0;
}
posted @ 2022-01-25 21:26  夜空之星  阅读(41)  评论(0编辑  收藏  举报