Evanyou Blog 彩带

网络流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,sp是每块新餐巾的费用; m是快洗部洗一块餐巾需用天数; f 是快洗部洗一块餐巾需要的费用; n是慢洗部洗一块餐巾需用天数; s是慢洗部洗一块餐巾需要的费用。

输出格式:

将餐厅在相继的 N 天里使用餐巾的最小总花费输出

输入输出样例

输入样例#1: 
3
1 7 5 
11 2 2 3 1
输出样例#1: 
134

说明

N<=2000

ri<=10000000

p,f,s<=10000

时限4s

Solution:

  分析:约束是每天的餐巾够用,目标是使费用最小。每天的餐巾有三种来源:新买的,m天前送到快洗部的,n天前送到慢洗部的。每天的餐巾有三种去路:延期处理,送到快洗部,送到慢洗部。网络流模型擅长处理的是小于等于号,然而这里是“够用”即大于等于。(1) 如果总是存在一种刚好够用的方案,它显然优于其他有冗余的方案。今天多用一些餐巾的好处是能多洗一些餐巾以备后用……这是不必要的。今天多用的餐巾如果是买的,要用的那天再买,能省去清洗费用;如果是洗的,从它被使用的那天起延期处理,今天清洗即可,今天用不着使用它。(2) 刚好够用的方案总是存在。所以,根据(1)(2),在费用最小的目标下,“够用”可以改成“恰好够用”。

  1. 上面的分析中,我们区分了“今天使用的”和“今天清洗的”。把“今天清洗的”作为X集合,“今天使用的”作为Y集合,我们能够建立二分图模型。今天使用的=今天清洗的=今天的需求,S->Xi,Yi->T,容量为ri,费用为0,求最大流把它们流满即满足约束。它们必能满流,因为其他边的容量都是正无穷的,见下文。
  2. 新买的:S->Yi,容量inf,费用p。
  3. 快洗:Xi->Y(i+m),i+m<=N,容量inf,费用f。
  4. 慢洗:Xi->Y(i+n),i+n<=N,容量inf,费用s。
  5. 延期处理: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;
}

 

posted @ 2018-02-05 14:38 five20 阅读(...) 评论(...) 编辑 收藏
Live2D