网络流24题之餐巾计划问题
题目描述
一个餐厅在相继的 N天里,每天需用的餐巾数不尽相同。假设第 i 天需要 ri块餐巾( i=1,2,...,N)。餐厅可以购买新的餐巾,每块餐巾的费用为 ppp 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 n(n>m),其费用为 s 分(s<f)。
每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。
试设计一个算法为餐厅合理地安排好 N天中餐巾使用计划,使总的花费最小。编程找出一个最佳餐巾使用计划。
输入输出格式
输入格式:由标准输入提供输入数据。文件第 1 行有 1 个正整数 N,代表要安排餐巾使用计划的天数。
接下来的 N行是餐厅在相继的 N天里,每天需用的餐巾数。
最后一行包含5个正整数p,m,f,n,s 。p是每块新餐巾的费用; m是快洗部洗一块餐巾需用天数; f 是快洗部洗一块餐巾需要的费用; n是慢洗部洗一块餐巾需用天数; s是慢洗部洗一块餐巾需要的费用。
输出格式:将餐厅在相继的 N 天里使用餐巾的最小总花费输出
输入输出样例
3
1 7 5
11 2 2 3 1
134
说明
N<=2000
ri<=10000000
p,f,s<=10000
时限4s
Solution:
分析:约束是每天的餐巾够用,目标是使费用最小。每天的餐巾有三种来源:新买的,m天前送到快洗部的,n天前送到慢洗部的。每天的餐巾有三种去路:延期处理,送到快洗部,送到慢洗部。网络流模型擅长处理的是小于等于号,然而这里是“够用”即大于等于。(1) 如果总是存在一种刚好够用的方案,它显然优于其他有冗余的方案。今天多用一些餐巾的好处是能多洗一些餐巾以备后用……这是不必要的。今天多用的餐巾如果是买的,要用的那天再买,能省去清洗费用;如果是洗的,从它被使用的那天起延期处理,今天清洗即可,今天用不着使用它。(2) 刚好够用的方案总是存在。所以,根据(1)(2),在费用最小的目标下,“够用”可以改成“恰好够用”。
- 上面的分析中,我们区分了“今天使用的”和“今天清洗的”。把“今天清洗的”作为X集合,“今天使用的”作为Y集合,我们能够建立二分图模型。今天使用的=今天清洗的=今天的需求,S->Xi,Yi->T,容量为ri,费用为0,求最大流把它们流满即满足约束。它们必能满流,因为其他边的容量都是正无穷的,见下文。
- 新买的:S->Yi,容量inf,费用p。
- 快洗:Xi->Y(i+m),i+m<=N,容量inf,费用f。
- 慢洗:Xi->Y(i+n),i+n<=N,容量inf,费用s。
- 延期处理:Xi->X(i+1),i< N,容量inf,费用0。
就是这样。
这个模型使我欣赏的地方:
1. 通过分析,把>=转为=,由于流的容量限制(<=),流量最大时满足了约束(取得等号)。“最小费用”、“最大流”两个约束很好地统一了。
2. “从源点流出的=流入汇点的”——广义的流量平衡。平时我们讲流量平衡,都是以一个顶点为对象,流入=流出。别忽视了整体。
怎么建图:
建图细节比较多,对于每个点i,拆成i和i',i表示用的餐巾,i'表示脏餐巾,连接:
(s,i,r[i],p)表示在这一天买新餐巾
(i,t,r[i],0)表示这一天用了r[i]的餐巾
(s,i+n,r[i],0)表示这一天有r[i]条脏餐巾
if(i+ft<=n) ins(i+n,i+ft,inf,fp)注意特判,表示送去快洗,inf是因为这一天的脏餐巾不止这一天剩下的,还有之前剩下的
if(i+st<=n) ins(i+n,i+st,inf,sp)注意特判,表示送去慢洗,inf是因为这一天的脏餐巾不止这一天剩下的,还有之前剩下的
if(i<n) ins(i+n,i+n+1,inf,0)注意特判,表示这一天的脏餐巾剩到第二天
图解:
首先题目要求第i天有r[i]块干净的餐巾可以用,于是我们先画出一个最简单的图:
其中逗号左边的数代表流量上限,右边的数代表花费。
接下来题目又说可以快洗和慢洗,于是我们将第i天右边的节点分别向左边第i+m天的节点和第i+n天的节点连对应的边,前提是i+m或i+n小于等于天数:
此时一条流过绿色边的流就代表快洗了一张餐巾,流过红色边就代表慢洗。流向t就代表扔掉这张餐巾。
为什么要右边的点向左边的点连边呢?因为每天快洗+慢洗+扔掉的餐巾要刚好等于r[i],而右边的点本身就刚好受到r[i]的流量限制。
然后我们再观察一下构图,发现还是有问题:某一天用完的餐巾不一定要当天就快洗或慢洗,然后送给第i+m或i+n天用;也不一定用完了就扔掉。它可能留着以后再洗,所以要修改一下构图:
这个图看上去是没问题了,但事实上它有一个很严重的问题。我们的最终目的是要使中间所有r[i],0的边都流满,于是现在来模拟一条流(用下图中的棕色表示):
那么它的含义是:在第一天买进来1条餐巾,然后在第一天结束时送去了慢洗,第二天用完后就一直没再用。
那么这条餐巾实际上用了两天,但它只代表了从s到t的1的流量。也就是说,如果有一些餐巾用了多天,最终的流量就会小于r的总和。而我们知道,最小费用最大流是优先跑最大流的,所以最后解出来的流一定是这样:
因为这样才是最大流量。也就是说,最终答案是p*(r的总和)。这样显然错误。
那有没有什么办法,让一条用了两天的餐巾代表2的流量呢?这就是本题构图的巧妙之处。我们不妨先让每天开始时得到的r[i]条干净的餐巾(左边一列的节点)流向t,然后再在每天结束时从s补回r[i]条脏的餐巾(从s向右边一列的节点连r[i],0的边):
这样,之前的那条棕色的流就被拆成了下面两条流:
然后问题就巧妙地解决啦!(注意流量要开long long)
代码:
#include<bits/stdc++.h> #define il inline #define ll long long #define debug printf("%d %s/n",__LINE__,__FUNCTION__) using namespace std; const ll maxn=100005,inf=23333333333333; ll N,n,p,m,f,s,h[maxn],cnt,ans,dis[maxn]; struct edge{ ll to,net,cos,v; }e[maxn<<1]; bool vis[maxn]; il ll gi() { ll a=0;char x=getchar();bool f=0; while((x<'0'||x>'9')&&x!='-')x=getchar(); if(x=='-')x=getchar(),f=1; while(x>='0'&&x<='9')a=a*10+x-48,x=getchar(); return f?-a:a; } il void add(ll u,ll v,ll w,ll cos) { e[cnt].to=v,e[cnt].net=h[u],e[cnt].cos=cos,e[cnt].v=w,h[u]=cnt++; e[cnt].to=u,e[cnt].net=h[v],e[cnt].cos=-cos,e[cnt].v=0,h[v]=cnt++; } il bool spfa() { deque<ll>q; memset(dis,-1,sizeof(dis)); memset(vis,0,sizeof(vis)); dis[N*2+1]=0;q.push_back(N*2+1); while(!q.empty()) { ll u=q.front();q.pop_front(); for(ll i=h[u];i!=-1;i=e[i].net) { ll v=e[i].to; if(e[i^1].v>0){ if(dis[v]>dis[u]-e[i].cos||dis[v]==-1){ dis[v]=dis[u]-e[i].cos; if(!vis[v]){ if(!q.empty()&&dis[q.front()]>dis[v])q.push_front(v); else q.push_back(v); } vis[v]=1; } } } vis[u]=0; } return dis[0]!=-1; } il ll getmin_cost(ll u,ll op) { vis[u]=1; if(u==N*2+1)return op; ll used,flow=0; for(ll i=h[u];i!=-1;i=e[i].net) { if(op<=0)break; ll v=e[i].to; if(!vis[v]&&e[i].v>0&&dis[v]+e[i].cos==dis[u]&&(used=getmin_cost(v,min(op,e[i].v)))>0) { e[i].v-=used;e[i^1].v+=used; op-=used;ans+=used*e[i].cos;flow+=used; } } return flow; } int main() { memset(h,-1,sizeof(h)); N=gi(); ll w; for(int i=1;i<=N;i++)w=gi(),add(i,N*2+1,w,0),add(0,i+N,w,0); p=gi(),m=gi(),f=gi(),n=gi(),s=gi(); for(int i=1;i<=N;i++) { add(0,i,inf,p); if(N>=i+m)add(i+N,i+m,inf,f); if(N>=i+n)add(i+N,i+n,inf,s); if(N>=i+1)add(i,i+1,inf,0); } while(spfa()){ memset(vis,0,sizeof(vis)); getmin_cost(0,inf); } printf("%lld\n",ans); return 0; }