[网络流24题]餐巾计划问题(费用流)
题面
一个餐厅在相继的 N天里,每天需用的餐巾数不尽相同。假设第 (i)天需要(r_i)块餐巾( i=1,2,...,N)。餐厅可以购买新的餐巾,每块餐巾的费用为 p 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 n 天(n>m),其费用为 s 分(s<f)。
每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。
试设计一个算法为餐厅合理地安排好 N 天中餐巾使用计划,使总的花费最小。编程找出一个最佳餐巾使用计划。
分析
这应该是网络流24题里思维难度最大的一道题。(不可做的机器人路径规划除外)
首先考虑本题的约束:每天的餐巾够用,还可以延期送洗。也就是说每天结束时的脏毛巾数(geq)每天使用的干净毛巾数,因为可能有来自前一天的脏毛巾。一般网络流由于流量上界的存在,擅长处理小于等于的问题,而这里是大于等于的问题,因此要么变换约束条件,要么用有上下界的网络流求解。
一般费用流的建图:
这里需要用到一个贪心结论:洗了的毛巾都被用了。这是因为如果使用的毛巾有一部分是洗的,那么等到之后再洗好也不迟。如果是买的,之后可以再买。
这样就可以拆点,(x)表示第(x)天产生的脏餐巾,(x+N)表示第(x)天获得的干净餐巾。又因为网络流的流量守恒有着两重意义:一是除源汇外点的流量守恒,二是源点流出总流量等于汇点汇入总流量,那么我们可以用源汇点(s,t)来分别表示产生脏餐巾和获得的干净餐巾,以保证洗了的毛巾都被用了。接下来就可以写出建图方式
- 连边((s,i,r_i,0)),表示产生(r_i)条脏毛巾
- 连边((i+N,t,r_i,0))表示获得(r_i)条干净毛巾
- 连边((i,i+1,+infin,0))表示延期送洗
- 连边((i,i+m+N,+infin,f))表示快洗,(i+m)天会获得干净毛巾
- 连边((i,i+n+N,+infin,s))表示慢洗,(i+n)天会获得干净毛巾
- 连边((s,i+N,infin,p))表示第(i)天直接购买新毛巾。
注意我们虽然拆了点,但不能直接将两个拆点相连,用两个拆点相连的边来表示限制。这是因为一般的最小费用流是建立在流量最大的基础上的,由于(s)连到(i),(i+N)连到(t),那么为了最大流,流会直接从(s ightarrow i ightarrow i+n ightarrow t),导致我们用链表示的意义(如本题中的延期送洗)失效。"[SNOI2019]通信"一题也利用了这个思想。
从有上下界的费用流建图:
前面提到的一般费用流建图总是有点反直觉(甚至感觉我解释的不太对)。而带上下界的费用流则很好理解。
同样拆点:第(x)天拆成(x)和(x+N).(x)表示每天开始,(x+N)表示每天结束。
- 连边((s,i,0,+infin,p))表示买新毛巾
- 连边((i,i+n,r_i,+infin,0)).流过这条边表示毛巾在这一天被操作(干净毛巾被使用或脏毛巾被送洗).([r_i ,+infin))表示每天结束时的脏毛巾数(geq)每天使用的干净毛巾数,因为可能有来自前一天延期送洗的脏毛巾。
- 连边((i,i+1,0,+infin,0))表示这些衣服在这一天不进行任何操作.即留到下一天再洗或使用。
- 连边((i+N,i+m,0,+infin,f))表示这一天结束时送洗毛巾,使用快洗,在第(i+m)天再决定是否被操作。
- 连边((i+N,i+n,0,+infin,s))表示这一天结束时送洗毛巾,使用慢洗。
- 连边((i+N,t,0,+infin,0))表示兀余的毛巾。
最小费用可行流即为答案。
这样看来有上下界的费用流建图很直接。流的意义从头至尾都是相同的,表示毛巾的流动,无论是脏的还是干净的。我们也不需要关心兀余是否有必要,直接建到图里让算法自己判断走不走即可。这样可以大大减少思维量,虽然图中的有些边可能永远不会被流过,且常数较大。
代码
费用流
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 10005
#define maxm 500005
#define INF 0x3f3f3f3f
using namespace std;
int n;
struct edge{
int from;
int to;
int next;
int flow;
int cost;
}E[maxm<<1];
int head[maxn];
int sz=1;
void add_edge(int u,int v,int w,int c){
// printf("%d->%d vol=%d cost=%d
",u,v,w,c);
sz++;
E[sz].from=u;
E[sz].to=v;
E[sz].next=head[u];
E[sz].flow=w;
E[sz].cost=c;
head[u]=sz;
sz++;
E[sz].from=v;
E[sz].to=u;
E[sz].next=head[v];
E[sz].flow=0;
E[sz].cost=-c;
head[v]=sz;
}
int dist[maxn];
int minf[maxn];
int pre[maxn];
int inq[maxn];
bool spfa(int s,int t){
memset(dist,0x3f,sizeof(dist));
memset(inq,0,sizeof(inq));
queue<int>q;
q.push(s);
inq[s]=1;
dist[s]=0;
while(!q.empty()){
int x=q.front();
q.pop();
inq[x]=0;
for(int i=head[x];i;i=E[i].next){
int y=E[i].to;
if(E[i].flow){
if(dist[y]>dist[x]+E[i].cost){
dist[y]=dist[x]+E[i].cost;
minf[y]=min(minf[x],E[i].flow);
pre[y]=i;
if(!inq[y]){
inq[y]=1;
q.push(y);
}
}
}
}
}
if(dist[t]==INF) return 0;
else return 1;
}
void update(int s,int t){
int x=t;
while(x!=s){
int i=pre[x];
E[i].flow-=minf[t];
E[i^1].flow+=minf[t];
x=E[i^1].to;
}
}
int mcmf(int s,int t){
memset(minf,0x3f,sizeof(minf));
int mincost=0,maxflow=0;
while(spfa(s,t)){
update(s,t);
mincost+=dist[t]*minf[t];
maxflow+=minf[t];
}
return mincost;
}
int p,fcost,fday,scost,sday;
int r[maxn];
int main(){
scanf("%d",&n);
scanf("%d %d %d %d %d",&p,&fday,&fcost,&sday,&scost);
for(int i=1;i<=n;i++) scanf("%d",&r[i]);
int s=0,t=n*2+1;
for(int i=1;i<=n;i++){
add_edge(s,i,r[i],0);
add_edge(i+n,t,r[i],0);
}
for(int i=1;i<n;i++){
add_edge(i,i+1,INF,0);
}
for(int i=1;i+fday<=n;i++){
add_edge(i,i+fday+n,INF,fcost);
}
for(int i=1;i+sday<=n;i++){
add_edge(i,i+sday+n,INF,scost);
}
for(int i=1;i<=n;i++){
add_edge(s,i+n,INF,p);
}
printf("%d
",mcmf(s,t));
}
有上下界的费用流:
//https://www.cnblogs.com/five20/p/8417493.html
//此题会爆int
//洛谷输入格式和loj不一样
#include<iostream>
#include<cstdio>
#include<cstring>
#include<queue>
#define maxn 10005
#define maxm 500005
#define INF 0x3f3f3f3f3f3f3f3f
using namespace std;
int n;
typedef long long ll;
namespace EK {
struct edge {
int from;
int to;
int next;
ll flow;
ll cost;
} E[maxm<<1];
int head[maxn];
int sz=1;
void add_edge(int u,int v,ll w,int c) {
// printf("%d->%d vol=%d cost=%d
",u,v,w,c);
sz++;
E[sz].from=u;
E[sz].to=v;
E[sz].next=head[u];
E[sz].flow=w;
E[sz].cost=c;
head[u]=sz;
sz++;
E[sz].from=v;
E[sz].to=u;
E[sz].next=head[v];
E[sz].flow=0;
E[sz].cost=-c;
head[v]=sz;
}
ll dist[maxn];
ll minf[maxn];
int pre[maxn];
int inq[maxn];
bool spfa(int s,int t) {
memset(dist,0x3f,sizeof(dist));
memset(inq,0,sizeof(inq));
queue<int>q;
q.push(s);
inq[s]=1;
dist[s]=0;
while(!q.empty()) {
int x=q.front();
q.pop();
inq[x]=0;
for(int i=head[x]; i; i=E[i].next) {
int y=E[i].to;
if(E[i].flow) {
if(dist[y]>dist[x]+E[i].cost) {
dist[y]=dist[x]+E[i].cost;
minf[y]=min(minf[x],E[i].flow);
pre[y]=i;
if(!inq[y]) {
inq[y]=1;
q.push(y);
}
}
}
}
}
if(dist[t]==INF) return 0;
else return 1;
}
void update(int s,int t) {
int x=t;
while(x!=s) {
int i=pre[x];
E[i].flow-=minf[t];
E[i^1].flow+=minf[t];
x=E[i^1].to;
}
}
pair<ll,ll> mcmf(int s,int t) {
memset(minf,0x3f,sizeof(minf));
ll mincost=0,maxflow=0;
while(spfa(s,t)) {
update(s,t);
mincost+=dist[t]*minf[t];
maxflow+=minf[t];
}
return make_pair(mincost,maxflow);
}
}
namespace bound_mcmf {
using namespace EK;
int cntv,cnte;
int from[maxm],to[maxm];
ll lower[maxm],upper[maxm];
ll cost[maxm];
ll dflow[maxn];//入流-出流
int hash_id[maxm];
void adde(int u,int v,ll l,ll r,ll c) {
// printf("%d->%d [%d,%d] cost=%d
",u,v,l,r,c);
cnte++;
from[cnte]=u;
to[cnte]=v;
lower[cnte]=l;
upper[cnte]=r;
cost[cnte]=c;
}
ll solve(int s,int t) {
ll ans=0;
int ss=cntv+1,tt=cntv+2;
adde(t,s,0,INF,0);
for(int i=1; i<=cnte; i++) {
add_edge(from[i],to[i],upper[i]-lower[i],cost[i]);
hash_id[i]=sz;
dflow[from[i]]-=lower[i];
dflow[to[i]]+=lower[i];
ans+=cost[i]*lower[i];
}
ll sum=0;
for(int i=1; i<=cntv; i++) {
if(dflow[i]<0) {
// if(i==1) printf("")
add_edge(i,tt,-dflow[i],0);
} else if(dflow[i]>0) {
add_edge(ss,i,dflow[i],0);
sum+=dflow[i];
}
}
pair<ll,ll> p=mcmf(ss,tt);
if(p.second==sum) {
// printf("flow1=%d
",p.first);
ans+=p.first;
E[hash_id[cnte]].flow=E[hash_id[cnte]^1].flow=0;
return ans;
} else return 0;
}
}
int p,fcost,fday,scost,sday;
int r[maxn];
int main() {
scanf("%d",&n);
for(int i=1; i<=n; i++) scanf("%d",&r[i]);
scanf("%d %d %d %d %d",&p,&fday,&fcost,&sday,&scost);
int s=0,t=n*2+1;
bound_mcmf::cntv=n*2+1;
//i今天清洗的,i+n今天使用的
for(int i=1; i<=n; i++) {
bound_mcmf::adde(s,i,0,INF,p);//买新毛巾
}
for(int i=1; i<n; i++) {
bound_mcmf::adde(i,i+1,0,INF,0);//脏毛巾可以留到下一天再洗
}
for(int i=1; i<=n; i++) {
bound_mcmf::adde(i,i+n,r[i],INF,0);
//当天洗的>=用的
}
for(int i=1;i<=n;i++){
bound_mcmf::adde(i+n,t,0,r[i],0);
}
for(int i=1; i+fday<=n; i++) {
bound_mcmf::adde(i+n,i+fday,0,INF,fcost);
}
for(int i=1; i+sday<=n; i++) {
bound_mcmf::adde(i+n,i+sday,0,INF,scost);
}
printf("%lld
",bound_mcmf::solve(s,t));
}