网络流学习笔记(三)——费用流

N总 觉得费用流太简单了,于是让我赶紧学习一下。

定义

费用:给网络的每一条边都有一个费用值 w,那么该条边的费用就是 wf,所有边的费用之和即为网络的费用。

费用流:一个流网络中,所有最大的可行流中,费用最小值(或最大值)。即最小费用最大流(或最大费用最大流)。

解法

只需要将 EK 算法 BFS 找增广路的过程改成 SPFA 即可。即求出到源点到汇点的边费用之和最小的路径。

证明交给 N总,本蒟蒻不会。

一些细节见模板题代码。

【模板】费用流

给定一个包含 n 个点 m 条边的有向图,并给定每条边的容量和费用,边的容量非负。

图中可能存在重边和自环,保证费用不会存在负环。

求从 ST 的最大流,以及在流量最大时的最小费用。

数据范围

2n5000,1m50000

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=10010;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
int n,m,s,t,idx=1,q[N],flow,cost,h[N],pre[N];
int d[N],incf[N];//d[i]表示从源点到i点时的所有路径中,最小的费用值之和,incf[i] 表示到 i 点时的流量 
struct edge{
	int v,w,nex,c;
}e[M];
bool vis[N];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
bool spfa()
{
	int hh=0,tt=1;
	memset(incf,0,sizeof(incf));
	memset(d,0x3f,sizeof(d));d[s]=0,q[0]=s;incf[s]=INF;
	while(hh!=tt)//注意多次入队的情况
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(e[i].w&&d[v]>d[u]+e[i].c)
			{
				d[v]=d[u]+e[i].c;pre[v]=i;
				incf[v]=min(incf[u],e[i].w);
				if(!vis[v])
				{
					vis[v]=true;q[tt++]=v;
					if(tt==N) tt=0;
				}
			}
		}
	}
	return incf[t]>0;
}
void EK()
{
	while(spfa())
	{
		flow+=incf[t],cost+=incf[t]*d[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	} 
} 
int main()
{
	scanf("%d%d%d%d",&n,&m,&s,&t);
	for(int u,v,w,c,i=1;i<=m;i++)
	{
		scanf("%d%d%d%d",&u,&v,&w,&c);
		add(u,v,w,c);add(v,u,0,-c);//注意此处的反向边费用值为-c,这是为了保证退流时费用减小 
	}
	EK();
	printf("%d %d\n",flow,cost);
	return 0;
}

【应用】运输问题

W 公司有 m 个仓库和 n 个零售商店。

i 个仓库有 ai 个单位的货物;第 j 个零售商店需要 bj 个单位的货物。

货物供需平衡,即 ai=bj

从第 i 个仓库运送每单位货物到第 j 个零售商店的费用为 cij

试设计一个将仓库中所有货物运送到零售商店的运输方案。

对于给定的 m 个仓库和 n 个零售商店间运送货物的费用,计算最优运输方案和最差运输方案。

数据范围

1m100,1n50

思路

把货物看成网络中的流量,把费用看成网络中的费用。那么本题就是一个多源汇费用流问题。首先可以新建虚拟源点和汇点,从源点向仓库 i 连一条费用为 0,容量为 ai 的边。商店向汇点连边同理。

由于本题保证了总货物数等于总需求数,那么最大流就必然为总货物数。题意就是要求最小费用最大流和最大费用最大流。

对于最大费用最大流,其实只需要将所有边的费用取反,最终输出的时候再输出相反数即可。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=210;
const int M=20020;
const int INF=0x3f3f3f3f;
int h[N],idx=1,s,t,n,m,incf[N],d[N],pre[N],q[N];
bool vis[N];
struct edge{
	int v,w,nex,c;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
bool spfa()
{
	int hh=0,tt=0;
	memset(incf,0,sizeof(incf));
	memset(d,0x3f,sizeof(d));
	d[s]=0,incf[s]=INF,q[tt++]=s;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
		     int v=e[i].v;
			 if(d[v]>d[u]+e[i].c&&e[i].w)
			 {
			 	 d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w);pre[v]=i;
			 	 if(!vis[v])
			     {
			 		vis[v]=true;q[tt++]=v;
			 		if(tt==N) tt=0;
				 }
			 }	
		}
	}
	return incf[t]>0;
}
int EK()
{
	int res=0;
	while(spfa())
	{
		res+=d[t]*incf[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	}
	return res;
}
int main()
{
	scanf("%d%d",&m,&n);s=0,t=n+m+1;
	for(int x,i=1;i<=m;i++) scanf("%d",&x),add(s,i,x,0),add(i,s,0,0);
	for(int x,i=1;i<=n;i++) scanf("%d",&x),add(i+m,t,x,0),add(t,i+m,0,0);
	for(int i=1;i<=m;i++)
	    for(int x,j=1;j<=n;j++)
	    {
	    	scanf("%d",&x);
	    	add(i,j+m,INF,x);add(j+m,i,0,-x);
		}
	printf("%d\n",EK());
	for(int i=2;i<idx;i+=2)//还原网络,费用取反 
	{
		e[i].w+=e[i^1].w;e[i^1].w=0;
		e[i].c=-e[i].c;e[i^1].c=-e[i^1].c;
	}
	printf("%d\n",-EK());
	return 0; 
} 

【建图】负载平衡问题

G 公司有 n 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等。如何用最少搬运量可以使 n 个仓库的库存数量相同。搬运货物时,只能在相邻的仓库之间搬运。

数据范围

1n100

思路

注意到一个货物有可能被搬运多次,所以只用流量的概念无法解决本题,需要用到费用的概念。

本题实质上是一道环形均分纸牌问题。有许多与简化版均分纸牌问题相同或相似的性质。如果把所有仓库的货物都减去最终的货物数。就可以从源点向正点连边,从负点向汇点连边。这里可以把本题的货物理解为流量,货物转移到另一个仓库要消耗费用。于是就可以向相邻两个节点连一条容量为 + (没有转移限制),费用为 1 (转移次数)的边。

由于最终要使所有仓库货物数相等,那么所有多的仓库都要把多的货物分出去;同理,少的仓库就要把少的货物都拿来。而多的货物一定等于少的货物(用简单的数学知识就可以证明),故网络的最大流就一定是多的货物的数量,此时也就保证了所有仓库都有相同的货物。

那么此时要求费用最小,直接跑一遍最小费用最大流即可。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=10010;
const int M=40010;
const int INF=1e7;
int q[N],incf[N],pre[N],d[N],h[N],idx=1,n,s,t,a[N];
struct edge{
	int v,w,nex,c;
}e[M];
bool vis[N];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;} 
bool spfa()
{
	int hh=0,tt=0;
	memset(incf,0,sizeof(incf));
	memset(d,0x3f,sizeof(d));d[s]=0,incf[s]=INF;q[tt++]=s;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(d[v]>d[u]+e[i].c&&e[i].w)
			{
				d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w);pre[v]=i;
				if(!vis[v])
				{
					vis[v]=true;q[tt++]=v;
					if(tt==N) tt=0;
				}
			}
		}
	}
	return incf[t]>0;
}
int EK()
{
	int res=0;
	while(spfa())
	{
		res+=d[t]*incf[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	}
	return res;
}
int main()
{
	int tot=0;scanf("%d",&n);s=0,t=n+1;
	for(int i=1;i<=n;i++)
	{
		scanf("%d",&a[i]),tot+=a[i];
		int u=i,v=i<n?i+1:1;
		add(u,v,INF,1);add(v,u,0,-1);
		v=i>1?i-1:n;
		add(u,v,INF,1);add(v,u,0,-1);
	}
	tot/=n;
	for(int i=1;i<=n;i++)
	{
		if(a[i]>tot) add(s,i,a[i]-tot,0),add(i,s,0,0);//注意从源点出发和到达汇点不需要费用
		else add(i,t,tot-a[i],0),add(t,i,0,0);
	}
	printf("%d\n",EK());
	return 0;
}

【二分图最优匹配】分配问题

n 件工作要分配给 n 个人做。

i 个人做第 j 件工作产生的效益为 cij

试设计一个将 n 件工作分配给 n 个人做的分配方案。

对于给定的 n 件工作和 n 个人,计算最优分配方案和最差分配方案。

数据范围

1n100

思路

显然可以用费用流求解,对于最差分配方案,只需将费用取反最终输出相反数即可。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=10010;
const int M=40010;
const int INF=0x3f3f3f3f;
int h[N],idx=1,s,t,n,pre[N],incf[N],d[N],q[N];
bool vis[N];
struct edge{
	int v,w,c,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
bool spfa()
{
	int hh=0,tt=0;
	memset(incf,0,sizeof(incf));memset(d,0x3f,sizeof(d));
	d[s]=0,incf[s]=INF;q[tt++]=s;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(d[v]>d[u]+e[i].c&&e[i].w)
			{
				d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w);pre[v]=i;
				if(!vis[v])
				{
					vis[v]=true;
					q[tt++]=v;if(tt==N) tt=0;
				}
			}
		}
	}
	return incf[t]>0;
}
int EK()
{
	int res=0;
	while(spfa())
	{
		res+=d[t]*incf[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	}
	return res;
	
}
int main()
{
	scanf("%d",&n);s=0,t=2*n+1;
	for(int u,v,i=1;i<=n;i++)
	{
		add(s,i,1,0);add(i,s,0,0);
		add(i+n,t,1,0);add(t,i+n,0,0);
		for(int x,j=1;j<=n;j++)
		{
			scanf("%d",&x);u=i,v=j+n;
			add(u,v,1,x);add(v,u,0,-x);
		}
	}
	printf("%d\n",EK());
	for(int i=2;i<idx;i+=2)
	{
		e[i].w=1,e[i^1].w=0;
		e[i].c=-e[i].c,e[i^1].c=-e[i^1].c;
	}
	printf("%d\n",-EK());
	return 0;
}

【最大权不相交路径】数字梯形问题

给定一个由 n 行数字组成的数字梯形如下图所示。

梯形的第一行有 m 个数字。

从梯形的顶部的 m 个数字开始,在每个数字处可以沿左下或右下方向移动,形成一条从梯形的顶至底的路径。

规则 1:从梯形的顶至底的 m 条路径互不相交。

规则 2:从梯形的顶至底的 m 条路径仅在数字结点处相交。

规则 3:从梯形的顶至底的 m 条路径允许在数字结点相交或边相交。

对于给定的数字梯形,分别按照规则 1,规则 2,和规则 3 计算出从梯形的顶至底的 m 条路径,使这 m 条路径经过的数字总和最大。

数据范围

1n,m20

思路

由于每个点只有选和不选两种状态,所以只用流量是无法求出最大数字和。需要用到费用的概念。对于点的限制,可以拆点,对于边的限制,可以设置边的流量。题目要求的是最大费用最大流,把 SPFA 求最短路改成求最长路即可。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=1210;
const int M=10010;
const int INF=0x3f3f3f3f;
int w[N][N],h[N],idx=1,n,m,s,t,pre[N],d[N],tag[N][N],incf[N],q[N],cnt;//为了方便表示,用cnt来记录每个点所对应的节点编号 
bool vis[N];
struct edge{
	int v,w,nex,c;
}e[M];
int min(int a,int b){return a<b?a:b; }
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
bool spfa()
{
	int hh=0,tt=0;
	memset(d,-0x3f,sizeof(d)),memset(incf,0,sizeof(incf));
	incf[s]=INF,d[s]=0;q[tt++]=s;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(d[v]<d[u]+e[i].c&&e[i].w)
			{
				d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w),pre[v]=i;
				if(!vis[v])
				{
					vis[v]=true;q[tt++]=v;
					if(tt==N) tt=0;
				}
			}
		}
	 } 
	 return incf[t]>0;
}
int EK()
{
	int res=0;
	while(spfa())
	{
		res+=incf[t]*d[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	}
	return res;
}
void solve1()
{
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=m+i-1;j++)
	    {
	    	if(i==1) add(s,tag[i][j]*2,1,0),add(tag[i][j]*2,s,0,0);
	    	if(i==n) add(tag[i][j]*2+1,t,1,0),add(t,tag[i][j]*2+1,0,0);
	    	if(i<n)
	    	{
	    		add(tag[i][j]*2+1,tag[i+1][j]*2,1,0);add(tag[i+1][j]*2,tag[i][j]*2+1,0,0);
	    		add(tag[i][j]*2+1,tag[i+1][j+1]*2,1,0);add(tag[i+1][j+1]*2,tag[i][j]*2+1,0,0);
			}
			add(tag[i][j]*2,tag[i][j]*2+1,1,w[i][j]);add(tag[i][j]*2+1,tag[i][j]*2,0,-w[i][j]);
		}
	printf("%d\n",EK());
}
void solve2()
{
	memset(h,-1,sizeof(h));idx=1;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=m+i-1;j++)
	    {
	    	if(i==1) add(s,tag[i][j]*2,1,0),add(tag[i][j]*2,s,0,0);
	    	if(i==n) add(tag[i][j]*2+1,t,INF,0),add(t,tag[i][j]*2+1,0,0);
	    	if(i<n)
	    	{
	    		add(tag[i][j]*2+1,tag[i+1][j]*2,1,0);add(tag[i+1][j]*2,tag[i][j]*2+1,0,0);
	    		add(tag[i][j]*2+1,tag[i+1][j+1]*2,1,0);add(tag[i+1][j+1]*2,tag[i][j]*2+1,0,0);
			}
			add(tag[i][j]*2,tag[i][j]*2+1,INF,w[i][j]);add(tag[i][j]*2+1,tag[i][j]*2,0,-w[i][j]);
		}
	printf("%d\n",EK());
}
void solve3()
{
	memset(h,-1,sizeof(h));idx=1;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=m+i-1;j++)
	    {
	    	if(i==1) add(s,tag[i][j]*2,1,0),add(tag[i][j]*2,s,0,0);
	    	if(i==n) add(tag[i][j]*2+1,t,INF,0),add(t,tag[i][j]*2+1,0,0);
	    	if(i<n)
	    	{
	    		add(tag[i][j]*2+1,tag[i+1][j]*2,INF,0);add(tag[i+1][j]*2,tag[i][j]*2+1,0,0);
	    		add(tag[i][j]*2+1,tag[i+1][j+1]*2,INF,0);add(tag[i+1][j+1]*2,tag[i][j]*2+1,0,0);
			}
			add(tag[i][j]*2,tag[i][j]*2+1,INF,w[i][j]);add(tag[i][j]*2+1,tag[i][j]*2,0,-w[i][j]);
		}
	printf("%d\n",EK());
}
int main()
{
	scanf("%d%d",&m,&n);s=++cnt,t=++cnt;
	for(int i=1;i<=n;i++)
	    for(int j=1;j<=m+i-1;j++)
	    	scanf("%d",&w[i][j]),tag[i][j]=++cnt;
	solve1();solve2();solve3();
	return 0;
}

【网格图模型】K取方格数

在一个 N×N 的矩形网格中,每个格子里都写着一个非负整数。

可以从左上角到右下角安排 K 条路线,每一步只能往下或往右,沿途经过的格子中的整数会被取走。

若多条路线重复经过一个格子,只取一次

求能取得的整数的和最大是多少。

数据范围

1N50,0K10

思路

和上一道题目类似。最主要的区别在于每一个点只能取一次。那么就可以拆点,把每个点 (i,j) 拆成两个点,从入点到出点连两条边,一条边的流量为 1,费用为 w[i][j],另一条边的流量为 +,费用为 0。这样既可保证每个点最多对答案贡献一次。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=5100;
const int M=30010;
const int INF=0x3f3f3f3f;
int h[N],idx=1,pre[N],d[N],incf[N],n,k,s,t,q[N];
bool vis[N];
struct edge{
	int v,w,nex,c;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
int get(int i,int j){return (i-1)*n+j;}
bool spfa()
{
	int hh=0,tt=0;
	memset(incf,0,sizeof(incf));memset(d,-0x3f,sizeof(d));
	q[tt++]=s;incf[s]=INF;d[s]=0;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(d[v]<d[u]+e[i].c&&e[i].w)
			{
				d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w);pre[v]=i;
				if(!vis[v])
				{
					vis[v]=true;q[tt++]=v;
					if(tt==N) tt=0;
				}
			}
		}
	}
	return incf[t]>0;
}
int EK()
{
	int res=0;
	while(spfa())
	{
		res+=d[t]*incf[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	}
	return res;
}
int main()
{
	scanf("%d%d",&n,&k);s=0,t=2*n*n+1;
	add(s,1,k,0),add(1,s,0,0);
	add(n*n*2,t,k,0),add(t,n*n*2,0,0);
	for(int i=1;i<=n;i++)
	    for(int w,j=1;j<=n;j++)
	    {
	    	scanf("%d",&w);
	    	if(i<n) add(get(i,j)*2,get(i+1,j)*2-1,INF,0),add(get(i+1,j)*2-1,get(i,j)*2,0,0);
	    	if(j<n) add(get(i,j)*2,get(i,j+1)*2-1,INF,0),add(get(i,j+1)*2-1,get(i,j)*2,0,0);
	    	add(get(i,j)*2-1,get(i,j)*2,1,w);add(get(i,j)*2,get(i,j)*2-1,0,-w);
	    	add(get(i,j)*2-1,get(i,j)*2,INF,0);add(get(i,j)*2,get(i,j)*2-1,0,0);
		}
	printf("%d\n",EK());
}

深海机器人问题

深海资源考察探险队的潜艇将到达深海的海底进行科学考察。

潜艇内有多个深海机器人。

潜艇到达深海海底后,深海机器人将离开潜艇向预定目标移动。

深海机器人在移动中还必须沿途采集海底生物标本。

沿途生物标本由最先遇到它的深海机器人完成采集。

每条预定路径上的生物标本的价值是已知的,而且生物标本只能被采集一次。

本题限定深海机器人只能从其出发位置沿着向北或向东的方向移动,而且多个深海机器人可以在同一时间占据同一位置。

用一个 P×Q 网格表示深海机器人的可移动位置。

西南角的坐标为 (0,0),东北角的坐标为 (P,Q)

给定每个深海机器人的出发位置和目标位置,以及每条网格边上生物标本的价值。

计算深海机器人的最优移动方案,使尽可能多的深海机器人到达目的地的前提下,采集到的生物标本的总价值最高。

数据范围

1P,Q15

思路

和上一道题类似,由于每个格子只能被取一次,那么需要建两条边。同时也需要注意一下网格的下标从零开始。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=1010;
const int M=1e5+10;
const int INF=0x3f3f3f3f;
int min(int a,int b){return a<b?a:b;}
int A,B,n,m,s,t,a,b,pre[N],h[N],idx=1,incf[N],d[N],q[N];
struct edge{
	int v,w,nex,c;
}e[M];
bool vis[N];
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
bool spfa()
{
	int hh=0,tt=0;
	memset(d,-0x3f,sizeof(d)),memset(incf,0,sizeof(incf));
	incf[s]=INF,d[s]=0;q[tt++]=s;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(d[v]<d[u]+e[i].c&&e[i].w)
			{
//				printf("%d %d %d\n",d[v],d[u],e[i].c);
				d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w);pre[v]=i;
				if(!vis[v])
				{
					vis[v]=true;q[tt++]=v;
					if(tt==N) tt=0;
				}
			}
		}
	}
	return incf[t]>0;
}
int EK()
{
	int res=0;
	while(spfa())
	{
		res+=d[t]*incf[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	}
	return res;
}
int get(int x,int y){return x*(m+1)+y;}
int main()
{
	scanf("%d%d%d%d",&A,&B,&n,&m);s=(n+1)*(m+1)+1,t=s+1;
	for(int i=0;i<=n;i++)
	    for(int c,j=0;j<m;j++)
	    {
	    	scanf("%d",&c);
	    	add(get(i,j),get(i,j+1),1,c),add(get(i,j+1),get(i,j),0,-c);
	    	add(get(i,j),get(i,j+1),INF,0),add(get(i,j+1),get(i,j),0,0);
		}
	for(int i=0;i<=m;i++)
	    for(int c,j=0;j<n;j++)
	    {
	    	scanf("%d",&c);
	    	add(get(j,i),get(j+1,i),1,c),add(get(j+1,i),get(j,i),0,-c);
	    	add(get(j,i),get(j+1,i),INF,0),add(get(j+1,i),get(j,i),0,0);
		}
	while(A--)
	{
		int x,y,k;
		scanf("%d%d%d",&k,&x,&y);
		add(s,get(x,y),k,0);add(get(x,y),s,0,0);
	}
	while(B--)
	{
		int x,y,k;
		scanf("%d%d%d",&k,&x,&y);
		add(get(x,y),t,k,0);add(t,get(x,y),0,0);
	}
	printf("%d\n",EK());
	return 0;
}

【拆点】餐巾计划问题

一个餐厅在相继的 N 天里,每天需用的餐巾数不尽相同。

假设第 i 天需要 ri 块餐巾 (i=1,2,,N)

餐厅可以购买新的餐巾,每块餐巾的费用为 p 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 n 天,其费用为 s 分。

餐厅每天使用的餐巾必须是今天刚购买的,或者是今天刚洗好的,且必须恰好提供 ri 块毛巾,不能多也不能少。

每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。

但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。

试设计一个算法为餐厅合理地安排好 N 天中餐巾使用计划,使总的花费最小。

数据范围

1N800
每天需用的餐巾数不超过 1000

思路

通过仔细分析,可以发现如果用一个点代表一天无法满足题意。于是就需要将每一天拆成两个点:留下的旧毛巾,需要的新毛巾。这样建图就十分方便了。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=4020;
const int M=20020;
const int INF=0x3f3f3f3f;
#define LL long long
int h[N],idx=1,q[N],d[N],incf[N],n,m,S,T,pre[N],nee[N];
bool vis[N];
struct edge{
	int v,w,c,nex;
}e[M];
int min(int a,int b){return a<b?a:b;}
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
bool spfa()
{
	int hh=0,tt=0;
	memset(d,0x3f,sizeof(d)),memset(incf,0,sizeof(incf));
	d[S]=0,incf[S]=INF,q[tt++]=S;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(d[v]>d[u]+e[i].c&&e[i].w)
			{
				d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w);pre[v]=i;
				if(!vis[v])
				{
					vis[v]=true;q[tt++]=v;
					if(tt==N) tt=0;
				}
			}
		}
	}
	return incf[T]>0;
}
LL EK()
{
	LL res=0;
	while(spfa())
	{
		res+=(LL)d[T]*incf[T];
		for(int u=T;u!=S;u=e[pre[u]^1].v) e[pre[u]].w-=incf[T],e[pre[u]^1].w+=incf[T];
	}
	return res;
}
int main()
{
	int pay,fast_day,fast_cost,slow_day,slow_cost;
	scanf("%d",&n);S=0,T=2*n+1;
	for(int i=1;i<=n;i++) scanf("%d",&nee[i]);
	scanf("%d%d%d%d%d",&pay,&fast_day,&fast_cost,&slow_day,&slow_cost);
	for(int i=1;i<=n;i++)
	{
		int need=nee[i];
		add(S,i*2-1,need,0),add(i*2-1,S,need,0);//这条边的含义是今天有need条旧毛巾可以给后面用 
		add(S,i*2,INF,pay),add(i*2,S,0,-pay);
		add(i*2,T,need,0),add(T,i*2,0,0);
		if(i<n) add(i*2-1,(i+1)*2-1,INF,0),add((i+1)*2-1,i*2-1,0,0);
		if(i+fast_day<=n) add(i*2-1,(i+fast_day)*2,INF,fast_cost),add((i+fast_day)*2,i*2-1,0,-fast_cost);
		if(i+slow_day<=n) add(i*2-1,(i+slow_day)*2,INF,slow_cost),add((i+slow_day)*2,i*2-1,0,-slow_cost);
	}
	printf("%lld\n",EK());
	return 0;
}

【上下界可行流】志愿者招募

申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管。

布布刚上任就遇到了一个难题:为即将启动的奥运新项目招募一批短期志愿者。

经过估算,这个项目需要 N 天才能完成,其中第 i 天至少需要 Ai 个人。

布布通过了解得知,一共有 M 类志愿者可以招募。

其中第 i 类可以从第 Si 天工作到第 Ti 天,招募费用是每人 Ci 元。

设计一种最优的招募方案。

数据保证一定有解。

数据范围

1N1000,1M10000

思路

本题对于每天人数的最少限制可以转化为该天与下一天连边的最小流量。那么就可以把本题转化为一个无源汇的上下界可行流问题。对于此类问题,常见的做法是将所有的边的流量减去该边流量的下界。

i 点减去入边的流量和为 a,出边的流量和为 b。设 a>b,那么此时如果新图中的经过这个点的流量为 0,那么实际上出边的流量和会比入边的流量多,就需要从虚拟源点向 i 点连一条容量为 ba 的边,实际上就是使得出边的最小流量为 ba,也就保证了网络的流量守恒。

对于一段可以在 [s,t] 内连续工作的志愿者,可以从 t+1s 连一条容量为 +,费用为 c 的边。这其实是保证了费用只会被计算一次。也就是每个流量直到经过了这条边后才知道它是哪一类志愿者(每个流量也可以对应多类志愿者),并且也会把 [s,t] 中没有经过的点(走已经经过了的点也没关系,因为在减去下界后的网络中,边的流量并不再代表当天的人数)再走一遍(也就是至少走了一个环才会走向汇点)。

code:

#include<cstdio>
#include<cstring>
using namespace std;
const int N=3030;
const int M=30030;
const int INF=0x3f3f3f3f;
int h[N],idx=1,n,m,s,t,pre[N],d[N],incf[N],q[N],tot;
int min(int a,int b){return a<b?a:b;}
struct edge{
	int v,w,nex,c;
}e[M];
bool vis[N];
void add(int u,int v,int w,int c){e[++idx].v=v;e[idx].w=w;e[idx].c=c;e[idx].nex=h[u];h[u]=idx;}
bool spfa()
{
	int hh=0,tt=0;
	memset(d,0x3f,sizeof(d)),memset(incf,0,sizeof(incf));
	d[s]=0,incf[s]=INF,q[tt++]=s;
	while(hh!=tt)
	{
		int u=q[hh++];vis[u]=false;
		if(hh==N) hh=0;
		for(int i=h[u];i;i=e[i].nex)
		{
			int v=e[i].v;
			if(d[v]>d[u]+e[i].c&&e[i].w)
			{
				d[v]=d[u]+e[i].c;incf[v]=min(incf[u],e[i].w);pre[v]=i;
				if(!vis[v]) 
				{
					vis[v]=true;q[tt++]=v;
					if(tt==N) tt=0;
				}
			}
		}
	}
	return incf[t]>0;
}
int EK()
{
	int res=0;
	while(spfa())
	{
		res+=d[t]*incf[t];
		for(int u=t;u!=s;u=e[pre[u]^1].v) e[pre[u]].w-=incf[t],e[pre[u]^1].w+=incf[t];
	}
	return res;
}
int main()
{
	scanf("%d%d",&n,&m);s=0,t=n+2;int last=0;
	for(int now,i=1;i<=n;i++)
	{
		scanf("%d",&now);
		if(now<last) add(s,i,last-now,0),add(i,s,0,0);
		if(now>last) tot+=last,add(i,t,now-last,0),add(t,i,0,0);
		add(i,i+1,INF,0),add(i+1,i,0,0);last=now;
	}
	add(s,n+1,last,0),add(n+1,s,0,0);
	for(int u,v,w,i=1;i<=m;i++)
	{
		scanf("%d%d%d",&u,&v,&w);
		add(v+1,u,INF,w),add(u,v+1,0,-w);
	}
	printf("%d\n",EK());
	return 0;
}
posted @   曙诚  阅读(571)  评论(0编辑  收藏  举报
编辑推荐:
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示