费用流相关例题解析
费用流模型的直接应用
运输问题
W 公司有 \(m\) 个仓库和 \(n\) 个零售商店。
第 \(i\) 个仓库有 \(a_i\) 个单位的货物;第 \(j\) 个零售商店需要 \(b_j\) 个单位的货物。
货物供需平衡,即 \(\sum\limits_{i=1}^{m} a_i=\sum\limits_{j=1}^{j=n} b_j\)。
从第 \(i\) 个仓库运送每单位货物到第 \(j\) 个零售商店的费用为 \(c_{i,j}\)。
试设计一个将仓库中所有货物运送到零售商店的运输方案。
对于给定的 \(m\) 个仓库和 \(n\) 个零售商店间运送货物的费用,计算最优运输方案和最差运输方案。
输入格式
第 \(1\) 行有 \(2\) 个正整数 \(m\) 和 \(n\),分别表示仓库数和零售商店数。
接下来的一行中有 \(m\) 个正整数 \(a_i\),表示第 \(i\) 个仓库有 \(a_i\) 个单位的货物。
再接下来的一行中有 \(n\) 个正整数 \(b_j\),表示第 \(j\) 个零售商店需要 \(b_j\) 个单位的货物。
接下来的 \(m\) 行,每行有 \(n\) 个整数,表示从第 \(i\) 个仓库运送每单位货物到第 \(j\) 个零售商店的费用 \(c_{i,j}\)。
输出格式
第一行输出最少运输费用。
第二行输出最多运输费用。
数据范围&时空要求
\(1≤m≤100,1≤n≤50,1≤ai≤30000,1≤bi≤60000,1≤cij≤1000\)
1s\64M
输入样例:
2 3
220 280
170 120 210
77 39 105
150 186 122
输出样例:
48500
69140
解析
本题非常的形象,我们把货物想象为流,那么就相当于流从仓库流向零售店。典型的多源汇问题。
我们可以类比最大流的做法。建立超级源点和汇点,源点向所有的仓库连一条边,容量为这个仓库的货物量 \(a_i\) ,费用是 \(0\) 。然后每个零售店向汇点连一条边,容量是零售店需求量 \(b_i\),费用也是 \(0\) 。每个仓库向每个零售店连边,由于运输量没有任何限制,所以容量设为 \(+\infty\) ,费用就是其对应的费用。
最大费用取反再求最小费用流就行了。
证明不给了,费用流几乎是在模拟本题的运输过程。
#include <bits/stdc++.h>
using namespace std;
const int N=1e4+10,M=2e5+10,INF=1e8;
int n,m,S,T;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y; cc[tot]=c; ww[tot]=d; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],incf[N],pre[N];
bool vis[N];
bool spfa()
{
int hh=0,tt=1;
memset(d,0x3f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S; d[S]=0,incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;
vis[x]=0;
for(int i=head[x]; ~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]>d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
void EK(int& flow,int& cost)
{
flow=cost=0;
while(spfa())
{
int tmp=incf[T];
flow+=tmp; cost+=d[T]*tmp;
for(int i=T;i!=S;i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
}
int main()
{
scanf("%d%d",&m,&n);
memset(head,-1,sizeof head);
S=0l,T=n+m+10;
for(int i=1;i<=m;i++)
{
int x;
scanf("%d",&x);
add(S,i,x,0);
}
for(int i=1;i<=n;i++)
{
int x;
scanf("%d",&x);
add(m+i,T,x,0);
}
for(int i=1;i<=m;i++)
{
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
add(i,m+j,INF,x);
}
}
int flow,cost;
EK(flow,cost);
printf("%d\n",cost);
for(int i=0;i<tot;i+=2)
{
cc[i]+=cc[i^1];cc[i^1]=0;
ww[i]=-ww[i];ww[i^1]=-ww[i^1];
}
EK(flow,cost);
printf("%d",-cost);
return 0;
}
负载平衡问题
\(G\) 公司有 \(n\) 个沿铁路运输线环形排列的仓库,每个仓库存储的货物数量不等。
如何用最少搬运量可以使 \(n\) 个仓库的库存数量相同。
搬运货物时,只能在相邻的仓库之间搬运。
数据保证一定有解。
输入格式
第 \(1\) 行中有 \(1\) 个正整数 \(n\),表示有 \(n\) 个仓库。
第 \(2\) 行中有 \(n\) 个正整数,表示 \(n\) 个仓库的库存量 \(a_i\)。
输出格式
输出最少搬运量。
数据范围及时空限制
\(1≤n≤100\),每个仓库的库存量不超过 \(100\)。
1s/64M
输入样例:
5
17 9 14 16 4
输出样例:
11
解析
我们首先可以算得到最终每个仓库有多少货物。设这个值为 \(\overline{a}\)
根据这个值,我们可以把仓库分为几类:
- \(a_i>\overline{a}\) 意味着这个仓库最终要向外给出一定货物。
- \(a_i=\overline{a}\) 意味着这个仓库出入货物应当平衡。
- \(a_i<\overline{a}\) 意味着这个仓库最终会接收一些货物。
由此,我们可以联想到类似于上图的建图方式。
我们把点分为三部分,一部分是 \(a_i>\overline{a}\) 的点,另一部分是 \(a_i<\overline{a}\) 的点,剩下的就是 \(a_i=\overline{a}\) 的点。
我们从源点向第一部分的每个点连边,容量为 \(a_i-\overline{a}\) ,费用为 \(0\) 。从第二部分的所有点向汇点连边,容量为 \(\overline{a}-a_i\) , 费用为 \(0\) 。相邻点之间连边,容量为 \(+\infty\) ,单位费用为 \(1\) 。
我们将流等价为货物,那么流在流网络之间的流动相当于货物在仓库之间的调动。而货物在仓库之间流动时会造成等于货物量的费用,相当于流流经边时创造流量值的费用。由于我们的货物最终全部到达第二部分点,也就是到达汇点,故最大流应该是一个满流(否则错解)。由于两问题中的费用值相同,所以所有的最简方案对应流网络之间的满流且最小费用流对应最小费用方案。
本题没有负环,所以直接打EK改就好了
#include <bits/stdc++.h>
using namespace std;
const int N=510, M=1e5+10, INF=1e8+10;
int n,S,T;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y; cc[tot]=c; ww[tot]=d; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],pre[N],incf[N];
bool vis[N];
int poi[N];
bool spfa()
{
int hh=0,tt=1;
memset(d,0x3f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S; d[S]=0, incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;
vis[x]=0;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]>d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
int EK()
{
int flow=0,cost=0;
while(spfa())
{
int tmp=incf[T];
flow+=tmp; cost+=d[T]*tmp;
for(int i=T;i!=S;i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
return cost;
}
int main()
{
scanf("%d",&n);
memset(head,-1,sizeof head);
S=0,T=n+1;
int sum=0;
for(int i=1;i<=n;i++)
{
scanf("%d",poi+i);
sum+=poi[i];
}
int a_=sum/n;
for(int i=1;i<=n;i++)
{
if(poi[i]>a_) add(S,i,poi[i]-a_,0);
if(poi[i]<a_) add(i,T,a_-poi[i],0);
if(i-1>0) add(i,i-1,INF,1);
else add(i,n,INF,1);
if(i+1<=n) add(i,i+1,INF,1);
else add(i,1,INF,1);
}
printf("%d",EK());
return 0;
}
分配问题
有 \(n\) 件工作要分配给 \(n\) 个人做。
第 \(i\) 个人做第 \(j\) 件工作产生的效益为 \(c_{i,j}\)。
试设计一个将 \(n\) 件工作分配给 \(n\) 个人做的分配方案。
对于给定的 \(n\) 件工作和 \(n\) 个人,计算最优分配方案和最差分配方案。
输入格式
第 \(1\) 行有 \(1\) 个正整数 \(n\),表示有 \(n\) 件工作要分配给 \(n\) 个人做。
接下来的 \(n\) 行中,每行有 \(n\) 个整数 \(c_{i,j}\),表示第 \(i\) 个人做第 \(j\) 件工作产生的效益为 \(c_{i,j}\)。
输出格式
第一行输出最差分配方案下的最小总效益。
第二行输出最优分配方案下的最大总效益。
数据范围及时空限制
\(1≤n≤50,0≤c_{i,j}≤100\)
1s/64M
输入样例:
5
2 2 2 1 2
2 3 1 2 4
2 0 1 1 1
2 3 4 3 3
3 2 1 2 1
输出样例:
5
14
解析
不穿衣服的二分图最优匹配。
我们把收益作为流,人数作为费用。
这个题要我们求的是最大匹配里边权最大的方案。
我们仍然考虑像飞行员配对一题中的方法建图,边流量设为 \(1\) 。原图中的边费用设为 \(c_{i,j}\) 其他边费用设为 \(0\)。EK改求最小费用流,最小/大费用就是最小/大效益。
#include <bits/stdc++.h>
using namespace std;
const int N=510,M=8e4+10,INF=1e8;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y; cc[tot]=c; ww[tot]=d; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d;nxt[tot]=head[y]; head[y]=tot++;
}
int q[N+5],d[N],incf[N],pre[N];
bool vis[N];
int n,S,T;
bool spfa()
{
int hh=0,tt=1;
memset(d,0x3f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S; d[S]=0; incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;//循环队列
vis[x]=0;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]>d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;//这里为了简洁+防止数组越界,我们将tt设高一位
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
int EK()//用引用来解决传出两个参数的问题
{
int flow=0,cost=0;
while(spfa())
{
int tmp=incf[T];
flow+=tmp; cost+=tmp*d[T];
for(int i=T; i!=S; i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
return cost;
}
int main()
{
scanf("%d",&n);
memset(head,-1,sizeof head);
S=0,T=2*n+1;
/*1~n人,n+1~2n工作*/
for(int i=1;i<=n;i++)
{
add(S,i,1,0);
add(n+i,T,1,0);
}
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
add(i,n+j,1,x);
}
}
printf("%d\n",EK());
for(int i=0;i<tot;i+=2)
{
cc[i]+=cc[i^1]; cc[i^1]=0;
ww[i]=-ww[i]; ww[i^1]=-ww[i^1];
}
printf("%d",-EK());
return 0;
}
最大权不相交路径
直接看题
数字梯形问题
给定一个由 \(n\) 行数字组成的数字梯形如下图所示。
梯形的第一行有 \(m\) 个数字。
从梯形的顶部的 \(m\) 个数字开始,在每个数字处可以沿左下或右下方向移动,形成一条从梯形的顶至底的路径。
有如下规则:
-
从梯形的顶至底的 \(m\) 条路径互不相交。
-
从梯形的顶至底的 \(m\) 条路径仅在数字结点处相交。
-
从梯形的顶至底的 \(m\) 条路径允许在数字结点相交或边相交。
对于给定的数字梯形,分别按照规则 \(1\),规则 \(2\),和规则 \(3\) 计算出从梯形的顶至底的 \(m\) 条路径,使这 \(m\) 条路径经过的数字总和最大。
输入格式
第 \(1\) 行中有 \(2\) 个正整数 \(m\) 和 \(n\),分别表示数字梯形的第一行有 \(m\) 个数字,共有 \(n\) 行。
接下来的 \(n\) 行是数字梯形中各行的数字。第 \(1\) 行有 \(m\) 个数字,第 \(2\) 行有 \(m+1\) 个数字,以此类推。
输出格式
将按照规则 \(1\),规则 \(2\),和规则 \(3\) 计算出的最大数字总和输出,每行输出一个最大总和。
数据范围及时空限制
\(1≤n,m≤20,\)
梯形中的数字范围 \([1,1000]\)。
1s/64M
输入样例:
2 5
2 3
3 4 5
9 10 9 1
1 1 10 1 1
1 1 10 12 1 1
输出样例:
66
75
77
解析
先分析第一个规则。
第一个规则是说每条边和每个点只能走一次。
也就是完全不相交的路径。
多源汇问题建立超级源汇点就能解决。
边只走一次很简单,我们把容量设为 \(1\) ,点只走一次,我们拆点即可。
由于我们的容量拿来限制每条边只走一次了,此时我们需要让费用来统计权值和。将每个点内边的费用设为权值。然后EK改跑最大费用流。
这也就是最大权不相交路径的做法。
分析第二个规则,点可以走多次
我们只需要在第一问的基础上将点内边容量修改为 \(+\infty\) 即可。
第三问同理,我们只要把所有边容量修改为正无穷即可。
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10,M=3e5+10,INF=1e8;
int n,m,S,T;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y; cc[tot]=c; ww[tot]=d; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d;nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],incf[N],pre[N];
bool vis[N];
int get(int x,int y)//x行y列
{
return (2*m+x)*x/2+y;
}
bool spfa()
{
int hh=0,tt=1;
memset(d,-0x3f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S; d[S]=0; incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;
vis[x]=0;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]<d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
int EK()
{
int cost=0;
while(spfa())
{
int tmp=incf[T];
cost+=d[T]*tmp;
for(int i=T; i!=S; i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
return cost;
}
int main()
{
scanf("%d%d",&m,&n);
memset(head,-1,sizeof head);
S=0,T=N-1;
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m+i-1;j++)
{
int x;
scanf("%d",&x);
int pos=get(i,j);
add(pos,1000+pos,1,x);//拆点
add(1000+pos,get(i+1,j),1,0);
add(1000+pos,get(i+1,j+1),1,0);
if(i==1) add(S,pos,1,0);
if(i==n) add(1000+pos,T,1,0);
}
}
printf("%d\n",EK());
for(int i=0;i<tot;i+=2)
{
int y=ver[i],x=ver[i^1];
if(y==1000+x||y==T) cc[i]=INF,cc[i^1]=0;
else cc[i]=1,cc[i^1]=0;
}
printf("%d\n",EK());
for(int i=0;i<tot;i+=2)
{
int x=ver[i^1];
if(x!=S) cc[i]=INF,cc[i^1]=0;
else cc[i]=1,cc[i^1]=0;
}
printf("%d\n",EK());
return 0;
}
网格图模型
K取方格数
在一个\(N\times N\) 的矩形网格中,每个格子里都写着一个非负整数。
可以从左上角到右下角安排 \(K\) 条路线,每一步只能往下或往右,沿途经过的格子中的整数会被取走。
若多条路线重复经过一个格子,只取一次。
求能取得的整数的和最大是多少。
输入格式
第一行包含两个整数 \(N\) 和 \(K\)。
接下来 \(N\) 行,每行包含 \(N\) 个不超过 \(1000\) 的整数,用来描述整个矩形网格。
输出格式
输出一个整数,表示能取得的最大和。
数据范围
\(1≤N≤50, 0≤K≤10\)
输入样例:
3 2
1 2 3
0 2 1
1 4 2
输出样例:
15
解析
当DP变成了暴力
我们现在先暂时不管点只能使用一次的问题,先把路径变成流。
把格子抽象为点,每个格子向右向下连两条边。
建立源点汇点,源点向左上角的点连边,右下角向汇点连边。
要选 \(k\) 条路线,所以我们将源汇点的连边容量设为 \(k\)。
中间这些边的容量是 \(+\infty\) 。
这样我们就建立出来了流网络。
此时我们发现这个网络的任何一个最大可行流都能和原问题的的一组 \(k\) 条路径方案相对应。
限制点的使用次数很简单,拆点即可。我们只能让一次经过拿到收益,那么我们在点内先连一条容量为 \(1\) 的带费用的边,再连一条容量为 \(+\infty\) 费用为 \(0\) 的边。
现在我们发现原问题的走法的点权和可以对应上最大流的费用了。
所以我们可以开开心心跑最大费用流了。
code:
#include <bits/stdc++.h>
using namespace std;
const int N=2e4+10, M=4e5+10, INF=1e8+10;
int n,k,S,T;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y; cc[tot]=c; ww[tot]=d; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],pre[N],incf[N];
bool vis[N];
inline int get(int x,int y)
{
return (x-1)*n+y;
}
bool spfa()
{
int hh=0,tt=1;
memset(d,-0x3f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S; d[S]=0,incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;
vis[x]=0;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]<d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
int EK()
{
int cost=0;
while(spfa())
{
int tmp=incf[T];
cost+=tmp*d[T];
for(int i=T;i!=S;i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
return cost;
}
int main()
{
scanf("%d%d",&n,&k);
memset(head,-1,sizeof head);
S=0,T=n*n*2+100;
int B=n*n+5;
add(S,1,k,0);
for(int i=1;i<=n;i++)
{
for(int j=1;j<=n;j++)
{
int x;
scanf("%d",&x);
int pos=get(i,j);
add(pos,B+pos,1,x);
add(pos,B+pos,INF,0);
if(i+1<=n) add(B+pos,get(i+1,j),INF,0);
if(j+1<=n) add(B+pos,get(i,j+1),INF,0);
}
}
add(B+get(n,n),T,k,0);
printf("%d",EK());
return 0;
}
深海机器人
深海资源考察探险队的潜艇将到达深海的海底进行科学考察。
潜艇内有多个深海机器人。
潜艇到达深海海底后,深海机器人将离开潜艇向预定目标移动。
深海机器人在移动中还必须沿途采集海底生物标本。
沿途生物标本由最先遇到它的深海机器人完成采集。
每条预定路径上的生物标本的价值是已知的,而且生物标本只能被采集一次。
本题限定深海机器人只能从其出发位置沿着向北或向东的方向移动,而且多个深海机器人可以在同一时间占据同一位置。
用一个 \(P×Q\) 网格表示深海机器人的可移动位置。
西南角的坐标为 \((0,0)\),东北角的坐标为 \((P,Q)\)。
给定每个深海机器人的出发位置和目标位置,以及每条网格边上生物标本的价值。
计算深海机器人的最优移动方案,使深海机器人到达目的地后,采集到的生物标本的总价值最高。
输入格式
第 \(1\) 行为深海机器人的出发位置数 \(a\),和目的地数 \(b\),第 \(2\) 行为 \(P\) 和 \(Q\) 的值。
接下来的 \(P+1\) 行,每行有 \(Q\) 个正整数,其中第 \(i\) 行(从 \(0\) 开始计数)的第 \(j\) 个(从 \(0\) 开始计数)正整数表示点 \((i,j)\) 到点 \((i,j+1)\) 的路径上生物标本的价值。
再接下来的 \(Q+1\) 行,每行有 \(P\) 个正整数,其中第 \(i\) 行(从 \(0\) 开始计数)的第 \(j\) 个(从 \(0\) 开始计数)正整数表示点 \((j,i)\) 到点 \((j+1,i)\) 的路径上生物标本的价值。
接下来的 \(a\) 行,每行有 \(3\) 个整数 \(k,x,y\),表示有 \(k\) 个深海机器人从 \((x,y)\) 位置坐标出发。
再接下来的 \(b\) 行,每行有 \(3\) 个整数 \(r,x,y\),表示有 \(r\) 个深海机器人可选择 \((x,y)\) 位置坐标作为目的地。
输出格式
输出采集到的生物标本的最高总价值。
数据范围
\(1≤a≤4,\\ 1≤b≤6,\\ 1≤P,Q≤15,\\ 1≤k,r≤10,\\ 0≤x≤P,\\ 0≤y≤Q,\)
各个生物标本价值不超过 \(200\)。
输入样例:
1 1
2 2
1 2
3 4
5 6
7 2
8 10
9 3
2 0 0
2 2 2
输出样例:
42
解析
题目略微冗杂,多读一会。
得到如下几点事项:
- 我们把数个机器人放到各个起点,让他们按照某种路线运动,机器人只能向上或向右走,且同一个点可以有多个机器人同时经过。
- 每条边上有生物标本,生物标本只能被采集一次,采集生物标本会获得其对应价值。
- 机器人只能在几个规定的终点回收,且每个终点回收机器人数量有上限。
- 我们要制定一种机器人放置及路线规划方案使其获得最大价值。求这个最大价值。
输入令人烦躁。
理清题意,我们发现这是一个多源汇的网格图模型。
首先套多源汇的套路,我们建立超级源点,向所有的起点连一条容量为 \(k\) 的边,\(k\) 为这个起点的机器人数量。所有的终点向超级汇点连一条 容量为 \(r\) 的边,\(r\) 为这个终点的回收数量上限。这些边费用都为 \(0\) 。
然后是套网格图的套路,我们根据其向右和向上的行走方向,可知道点 \((i,j)\) 向 \((i+1,j)\) 和 \((i,j+1)\) 连边。由于只有一次能采集到标本,我们将边分为两条,一条容量为 \(1\), 费用就是标本价值。另外一条容量 \(+\infty\) ,费用 \(0\)。
于是我们可以发现,此时原问题的任意一个行走方案可以对应到这个网络的一个最大流,且原问题方案的价值和对应最大流的费用也能够对应。
故我们最大化费用即可。
#include <bits/stdc++.h>
using namespace std;
const int N=2e5+10,M=1e6+10,INF=1e8+10;
int n,m,A,B,S,T;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y; cc[tot]=c; ww[tot]=d; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],incf[N],pre[N];
bool vis[N];
int get(int x,int y)//坐标->点编号
{
return x*16+y+1;
}
bool spfa()
{
int hh=0,tt=1;
memset(d,-0x3f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S; d[S]=0; incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;
vis[x]=0;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]<d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
int EK()
{
int cost=0;
while(spfa())
{
int tmp=incf[T];
cost+=d[T]*tmp;
for(int i=T; i!=S; i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
return cost;
}
int main()
{
scanf("%d%d%d%d",&A,&B,&n,&m);
memset(head,-1,sizeof head);
S=N-1,T=N-2;
for(int i=0;i<=n;i++)
{
for(int j=0;j<m;j++)//(i,j)->(i,j+1)
{
int x;
scanf("%d",&x);
add(get(i,j),get(i,j+1),1,x);
add(get(i,j),get(i,j+1),INF,0);
}
}
for(int i=0;i<=m;i++)
{
for(int j=0;j<n;j++)//(j,i)->(j+1,i)
{
int x;
scanf("%d",&x);
add(get(j,i),get(j+1,i),1,x);
add(get(j,i),get(j+1,i),INF,0);
}
}
for(int i=1;i<=A;i++)
{
int k,x,y;
scanf("%d%d%d",&k,&x,&y);
add(S,get(x,y),k,0);
}
for(int i=1;i<=B;i++)
{
int r,x,y;
scanf("%d%d%d",&r,&x,&y);
add(get(x,y),T,r,0);
}
printf("%d",EK());
return 0;
}
餐巾计划问题
一个餐厅在相继的 \(N\) 天里,每天需用的餐巾数不尽相同。
假设第 \(i\) 天需要 \(r_i\) 块餐巾 \((i=1,2,…,N)\)。
餐厅可以购买新的餐巾,每块餐巾的费用为 \(p\) 分;或者把旧餐巾送到快洗部,洗一块需 \(m\) 天,其费用为 \(f\) 分;或者送到慢洗部,洗一块需 \(n\) 天,其费用为 \(s\) 分。
餐厅每天使用的餐巾必须是今天刚购买的,或者是今天刚洗好的,且必须恰好提供 \(r_i\) 块毛巾,不能多也不能少。
每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。
但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。
试设计一个算法为餐厅合理地安排好 \(N\) 天中餐巾使用计划,使总的花费最小。
输入格式
第 \(1\) 行有 \(6\) 个正整数 \(N,p,m,f,n,s\) 。\(N\) 是要安排餐巾使用计划的天数;\(p\) 是每块新餐巾的费用;\(m\) 是快洗部洗一块餐巾需用天数;\(f\) 是快洗部洗一块餐巾需要的费用;\(n\) 是慢洗部洗一块餐巾需用天数;\(s\) 是慢洗部洗一块餐巾需要的费用。
接下来的 \(N\) 行是餐厅在相继的 \(N\) 天里,每天需用的餐巾数。
输出格式
输出餐厅在相继的 \(N\) 天里使用餐巾的最小总花费。
数据范围及时空限制
\(1≤N≤800, 1≤s<f<p≤50, 1≤m≤n≤20,\)
每天需用的餐巾数不超过 \(1000\)。
1s/64M
输入样例:
3 10 2 3 3 2
5
6
7
输出样例:
145
解析
我们将天数抽象为点,思考一天餐巾的来源和去向。
对于每天,我们需要 \(r_i\) 块干净的毛巾,这些毛巾只有三种来路:快洗部,慢洗部,购买。干净的毛巾不能继承。
对于每天,旧毛巾来路有两种:一种是今天用的,一种是前一天继承下来的。同时旧毛巾有三种出路:放到快洗部,放到慢洗部,放到下一天。
题目要求在满足条件的情况下,最小化总费用。
我们发现,一天有新旧两种毛巾需要考虑,且新毛巾会在这一天变为旧毛巾,所以我们考虑拆点,一个点考虑旧毛巾(出点),一个点考虑新毛巾(入点)。
对于购买操作,我们可以利用超级源点向入点连费用为 \(p\) 的边完成这一操作。对于快洗部操作,我们可以从 \(m\) 天前的出点连边到入点,容量为 \(+\infty\) ,费用为 \(f\) 。慢洗部同理。我们就解决的新毛巾的来源问题。最后每个入点向汇点连一条容量为 \(r_i\) ,费用为 \(0\) 的边。
下面我们着眼于旧毛巾的问题。每天我们会产生 \(r_i\) 条旧毛巾,所以我们可以从源点向每个出点连容量为 \(r_i\) 的边。我们的毛巾可以是上一天放过来的。所以我们从上一天的出点连一条容量为 \(+\infty\) 的边到当前出点。
总结一下:
对于第 \(i\) 天,我们建两个点 \(a_i,b_i\) ,分别处理旧毛巾,新毛巾。建立超级源汇点 \(s,t\) 。按如下规则建边:
- \(s\) 向所有的 \(a_i\) 连一条容量和费用为 \(\{r_i,0\}\) 的边,向所有的 \(b_i\) 连 \(\{+\infty,p\}\) 的边。
- 所有的 \(b_i\) 向 \(t\) 连 \(\{r_i,0\}\) 的边。
- \(a_i\) 向 \(b_{i+m}\) 连 \(\{+\infty,f\}\) 的边,向\(b_{i+n}\) 连 \(\{+\infty,s\}\) 的边。
- \(a_i\) 向 \(a_{i+1}\) 连 \(\{+\infty,0\}\) 的边。
这样我们就建完图了。
现在我们考虑原问题的什么样的方案会和这个网络的什么流对应。
显然应该是最大流。因为原问题提到我们每天必须要消耗 \(r_i\) 块毛巾,也就是说到汇点的边都应该是满流的。显然这应该是一个最大流。
但是原问题的任意一个方案都是最大流吗?不一定,我们应该保证毛巾正好用完。但是容易想到的是,最小费用的方案一定在这个子集中。
所以放心大胆EK改
#include <bits/stdc++.h>
using namespace std;
const int N=1e5+10, M=2e6+10,INF=1e8+10;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y, cc[tot]=c; ww[tot]=d, nxt[tot]=head[x], head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],incf[N],pre[N];
bool vis[N];
int n,p,fn,f,sn,s,S,T;
bool spfa()
{
int hh=0,tt=1;
memset(d,0x3f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S; d[S]=0; incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;
vis[x]=0;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]>d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
int EK()
{
int cost=0;
while(spfa())
{
int tmp=incf[T];
cost+=d[T]*tmp;
for(int i=T;i!=S;i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
return cost;
}
int main()
{
scanf("%d%d%d%d%d%d",&n,&p,&fn,&f,&sn,&s);
memset(head,-1,sizeof head);
S=0,T=n*2+10;
/*1~n 旧毛巾,n+1~2n 新毛巾*/
for(int i=1;i<=n;i++)
{
int r;
scanf("%d",&r);
add(S,i,r,0);
add(n+i,T,r,0);//与限制 r 有关的边
}
for(int i=1;i<=n;i++)
{
add(S,n+i,INF,p);//购买
if(i+fn<=n) add(i,n+i+fn,INF,f);
if(i+sn<=n) add(i,n+i+sn,INF,s);//快洗部和慢洗部
if(i<n) add(i,i+1,INF,0);//放到下一天
}
printf("%d",EK());
return 0;
}
上下界可行流
志愿者招募
申奥成功后,布布经过不懈努力,终于成为奥组委下属公司人力资源部门的主管。
布布刚上任就遇到了一个难题:为即将启动的奥运新项目招募一批短期志愿者。
经过估算,这个项目需要 \(N\) 天才能完成,其中第 \(i\) 天至少需要 \(A_i\) 个人。
布布通过了解得知,一共有 \(M\) 类志愿者可以招募。
其中第 \(i\) 类可以从第 \(S_i\) 天工作到第 \(T_i\) 天,招募费用是每人 \(C_i\) 元。
新官上任三把火,为了出色地完成自己的工作,布布希望用尽量少的费用招募足够的志愿者,但这并不是他的特长!
于是布布找到了你,希望你帮他设计一种最优的招募方案。
数据保证一定有解。
输入格式
第一行包含两个整数 \(N, M\) ,表示完成项目的天数和可以招募的志愿者的种类。
接下来的一行中包含 \(N\) 个非负整数,表示每天至少需要的志愿者人数。
接下来的 \(M\) 行中每行包含三个整数 \(S_i,T_i,C_i\),含义如上文所述。
为了方便起见,我们可以认为每类志愿者的数量都是无限多的。
输出格式
包含一个整数,表示你所设计的最优方案的总费用。
数据范围及时空限制
30%的数据中,\(1≤N,M≤10\),\(1≤Ai≤10\)
100%的数据中,\(1≤N≤1000,1≤M≤10000\)
数据保证题目中其他所涉及的数据以及答案均不超过\(2^{31}−1\)。
1s/64M
输入样例:
3 3
2 3 4
1 2 2
2 3 5
3 3 2
输出样例:
14
样例解释
招募 \(3\) 名第一类志愿者和 \(4\) 名第三类志愿者。
解析
本题建图十分清奇。
我们使用一条边来表示一天。
也就是说我们要是想要表示六天,我们就先如下建图:
也就是说,第 \(i\) 个点到第 \(i+1\) 个点之间的边就代表第 \(i\) 天。
我们将人想象为流,每天都有一个最少人数,那这样的话每天都有一个下界。
现在考虑如何在图中体现出志愿者的限制。
我们假设有一类志愿者从第 \(2\) 天工作到第 \(3\) 天,相当于这类志愿者可以从第二条边 \((2,3)\) 开始流过两条边。我们要是想保证流量守恒,可以拿一条边把 \(4\) 和 \(2\) 接起来。这样,我们还会发现这类志愿者的取用人数就是我们连的这条边的流量,同时满足了流量守恒。
每类志愿者都有无限人,我们把这条边的容量设为 \(+\infty\),费用设为 \(c_i\) ,即每人的费用。
这个题就变成了无源汇有上下界最小费用可行流。
我们很容易证得原图的任意方案和本网络的一个可行流对应。直接构造就是。
现在的问题是怎么求解这个无源汇有上下界最小费用可行流。
我们当初在做无源汇有上下界可行流的时候是先所有边减去下界,发现流量守恒被破坏后我们要根据每个点连边。对每个点 \(u\) 分两种情况 \(C_{入}>C_{出}\) 时 连接 \(c(S,u)=C_{入}-C_{出}\) ;当 \(C_{入}<C_{出}\) 时连接 \(c(u,T)=|C_{出}-C_{入}|\) ,依此建立一个新图 \(G^{\prime}\)
我们当时已经证明过,原图中的任意一个可行流对应这个图里的满流。
现在我们要把费用插进去,由于本题我们的红色边是没有下界的,所以对建图没有任何影响。我们可以直接在上面问题的基础上求解最小费用的满流。
问题已经转换为了最小费用最大流问题,EK改就能解决。
#include <bits/stdc++.h>
using namespace std;
const int N=4e3+10 ,M=4e4+10, INF=1e8;
int head[N],ver[M],nxt[M],cc[M],ww[M],tot=0;
void add(int x,int y,int c,int d)
{
ver[tot]=y; cc[tot]=c; ww[tot]=d; nxt[tot]=head[x]; head[x]=tot++;
ver[tot]=x; cc[tot]=0; ww[tot]=-d; nxt[tot]=head[y]; head[y]=tot++;
}
int q[N],d[N],incf[N],pre[N];
bool vis[N];
int n,m,S,T;
bool spfa()
{
int hh=0,tt=1;
memset(d,0x7f,sizeof d);
memset(incf,0,sizeof incf);
q[0]=S,d[S]=0,incf[S]=INF;
while(hh!=tt)
{
int x=q[hh++];
if(hh==N) hh=0;
vis[x]=0;
for(int i=head[x];~i;i=nxt[i])
{
int y=ver[i];
if(cc[i] && d[y]>d[x]+ww[i])
{
d[y]=d[x]+ww[i];
pre[y]=i;
incf[y]=min(cc[i],incf[x]);
if(!vis[y])
{
q[tt++]=y;
if(tt==N) tt=0;
vis[y]=1;
}
}
}
}
return incf[T]>0;
}
int EK()
{
int cost=0;
while(spfa())
{
int tmp=incf[T];
cost+=d[T]*tmp;
for(int i=T;i!=S;i=ver[pre[i]^1])
{
cc[pre[i]]-=tmp;
cc[pre[i]^1]+=tmp;
}
}
return cost;
}
int main()
{
scanf("%d%d",&n,&m);
S=n+5,T=S+1;
memset(head,-1,sizeof head);
int lst=0;
/*由于本题带下界的边组成了一条链,所以C(入)就是上一天的人数下界,C(出)就是这一天的人数下界*/
for(int i=1;i<=n;i++)
{
int r;
scanf("%d",&r);//读入当前人数
add(i,i+1,INF-r,0);
if(lst>r) add(S,i,lst-r,0);
else if(lst<r) add(i,T,r-lst,0);//向源汇点连边
lst=r;
}
add(S,n+1,lst,0);//最后一个点特殊情况
for(int i=1;i<=m;i++)
{
int a,b,c;
scanf("%d%d%d",&a,&b,&c);
add(b+1,a,INF,c);
}
printf("%d",EK());//直接求G'最小费用最大流
return 0;
}