zoukankan      html  css  js  c++  java
  • 网络流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
    ",ans);
        return 0;
    }
  • 相关阅读:
    Windows统一平台: 开发小技巧
    How to install more voices to Windows Speech?
    Why does my ListView scroll to the top when navigating backwards?
    中文圣经 for Android
    [ CodeVS冲杯之路 ] P1166
    [ CodeVS冲杯之路 ] P1154
    [ CodeVS冲杯之路 ] P1048
    [ CodeVS冲杯之路 ] P1063
    [ CodeVS冲杯之路 ] P3027
    理解矩阵乘法
  • 原文地址:https://www.cnblogs.com/five20/p/8417493.html
Copyright © 2011-2022 走看看