[网络流24题]餐巾计划问题——费用流建模
不错的建模题。
满足餐巾需求之下,花费最小。可以想到费用流。
但是怎么建模呢?
可以想到,因为N<=2000,而且一切的工作,洗刷,购买都和天有关系。
所以,肯定要把网络流中的点看做每一天。
比较麻烦的是,我们不好处理餐巾的干净和脏的状态,
如果每天只有“一个点”的话,我们也不能处理一个餐巾由干净变成脏的情况。(我们不能区分,一个用完的脏餐巾往下传递,和一个干净餐巾留到明天)
所以,考虑把天拆点。
那么,自然而然地,
一天拆成早上和晚上。
晚上会获得早上用完的脏毛巾
早上有的毛巾必须是干净的,才能用。
这样连边:
1.S向每个晚上连流ri费0,表示每天晚上会获得这么多的脏毛巾
2.每个早上向T连流ri费0,表示每天早上会用这么多的干净毛巾,并且可以限制最大流一定是sigma(ri)
3.购买,洗刷就比较自然了。
①S向每个早上,连接流inf费p,意义即买
②晚上的i,向走早上的i+day1连接流inf费cost1 ,意义即快洗部。慢洗部同理
4.还有一个情况是,可能我脏毛巾留着,等到最后要洗的时候,才拿去洗
所以,晚上i向i+1连流inf费0的边。
(当然,你也可以比较勤奋,反正以后要用,今天就先洗了。
早上i向i+1连流inf费0的边也可以。,
意义即,干净的毛巾留下来。
其实就是一个平行四边形。。
黑色是脏毛巾留下来,
红色是干净毛巾留下来。
一个提前洗,一个到时候再洗
)
这样,
我可以实现:干净毛巾转化成脏毛巾,脏毛巾洗成干净毛巾,购买干净毛巾,脏毛巾的积累。
跑费用流即可e
#include<bits/stdc++.h> #define reg register int #define il inline #define numb (ch^'0') using namespace std; typedef long long ll; il void rd(int &x){ char ch;x=0;bool fl=false; while(!isdigit(ch=getchar()))(ch=='-')&&(fl=true); for(x=numb;isdigit(ch=getchar());x=x*10+numb); (fl==true)&&(x=-x); } namespace Miracle{ const int N=4005; const int M=2000*6+23; const ll inf=0x3f3f3f3f3f3f3f3fll; struct node{ int nxt,to; ll w,v; }e[2*M]; int hd[N],cnt=1; int n,m,s,t; ll p,d1,c1,d2,c2; void add(int x,int y,ll z,ll c){ e[++cnt].nxt=hd[x];e[cnt].to=y; e[cnt].w=z;e[cnt].v=c; hd[x]=cnt; e[++cnt].nxt=hd[y];e[cnt].to=x; e[cnt].w=0;e[cnt].v=-c; hd[y]=cnt; } int pre[N]; ll incf[N]; ll d[N]; queue<int>q; bool in[N]; bool spfa(){ while(!q.empty()) q.pop(); memset(d,0x3f,sizeof d); d[s]=0; q.push(s); pre[s]=0;incf[s]=inf; while(!q.empty()){ int x=q.front();q.pop(); in[x]=0; for(reg i=hd[x];i;i=e[i].nxt){ int y=e[i].to; if(e[i].w){ if(d[y]>d[x]+e[i].v){ d[y]=d[x]+e[i].v; pre[y]=i; incf[y]=min(incf[x],e[i].w); if(!in[y]){ in[y]=1; q.push(y); } } } } } if(d[t]==inf) return false; return true; } ll ans; void upda(){ int x=t; while(pre[x]){ e[pre[x]].w-=incf[t]; e[pre[x]^1].w+=incf[t]; x=e[pre[x]^1].to; } ans+=d[t]*incf[t]; } int main(){ rd(n); s=2*n+1,t=2*n+2; ll x; for(reg i=1;i<=n;++i){ scanf("%lld",&x); add(s,i,x,0); add(i+n,t,x,0); } scanf("%lld%lld%lld%lld%lld",&p,&d1,&c1,&d2,&c2); for(reg i=1;i<=n;++i){ add(s,i+n,0x3f3f3f3f,p); if(i+d1<=n) add(i,i+n+d1,inf,c1); if(i+d2<=n) add(i,i+n+d2,inf,c2); if(i+1<=n) add(i,i+1,inf,0); } while(spfa()) upda(); printf("%lld",ans); return 0; } } int main(){ Miracle::main(); return 0; } /* Author: *Miracle* Date: 2018/11/28 8:21:33 */
总结:
为什么要拆点?
因为同一个物品在不同时空下的状态是不同的,之间的决策也相对独立,甚至可能存在相互转移的情况。
当一个点表示已经无能为力的时候,可以考虑拆点。
例如修车、美食节
(化点为边,,,,严格意义上不算是,这只是为了方便统计处理,并不是状态不同)
只要能处理好拆出来的点之间的关系就好。