打打打打打字机|

realFish

园龄:3年2个月粉丝:3关注:0

洛谷 P1251餐巾计划问题 题解

题面

传送门

描述

一个餐厅在相继的 N 天里,每天需用的餐巾数不尽相同。假设第 i 天需要 ri 块餐巾(i=1,2,...,N)。餐厅可以购买新的餐巾,每块餐巾的费用为 p 分;或者把旧餐巾送到快洗部,洗一块需 m 天,其费用为 f 分;或者送到慢洗部,洗一块需 n 天(n>m),其费用为 s 分(s<f)。
每天结束时,餐厅必须决定将多少块脏的餐巾送到快洗部,多少块餐巾送到慢洗部,以及多少块保存起来延期送洗。但是每天洗好的餐巾和购买的新餐巾数之和,要满足当天的需求量。
试设计一个算法为餐厅合理地安排好 N 天中餐巾使用计划,使总的花费最小。编程找出一个最佳餐巾使用计划。

输入格式

第 1 行有 1 个正整数 N,代表要安排餐巾使用计划的天数。
接下来的一行是餐厅在相继的 N 天里,每天需用的餐巾数。
最后一行包含5个正整数p,m,f,n,sp 是每块新餐巾的费用; m 是快洗部洗一块餐巾需用天数; f 是快洗部洗一块餐巾需要的费用; n 是慢洗部洗一块餐巾需用天数; s 是慢洗部洗一块餐巾需要的费用。

输出格式

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

题解

不难看出,这是一道最小费用流的题目。此类问题建模时,一定要注意“最小费用基于最大流”,要使问题的最终答案在网络中一定对应一个最大流才行。否则辛苦打代码,等到输出时才发现建模错误,那会浪费不少时间。(亲身经历)
首先,将每天看做一个点,那么流入这个点的流量不能小于r。题面中的“每天结束时”也启发我们用点边转化的方式,即拆点,处理此题。
不难想到一个看似正确的做法:将每个点拆成“早上的点”与“晚上的点”,并从早点向晚点连(r,0)(容量为r,费用为0)的边。设一个虚拟源点S,从S向所有早点连(+,p)的边,表示每天早上可以买任意条餐巾。设虚拟汇点T(垃圾桶),从所有晚点向T(+,0)的边,表示脏餐巾可以任意丢弃。在从每个第x天的晚点向第x+m天的早点连(rx,f)的边,表示快洗,慢洗同理;另外,每天早上的餐巾可以留到下一天,从x天的早点向x+1天的早点连(+,0)的边。
然后在这张网络上跑费用流模板。样例炸了。此时模拟样例就会发现:这种网络可以保证答案对应的可行流f每天的流量一定等于r,但不保证在流入汇点时f仍为最大流!换言之,虽然可以建立原问题与可行流的对应关系,但原问题的解不是最大流,也自然不能用费用流的方式求解。只能重新建模。
f在此网络上不是最大流,原因在于晚点连向汇点的边。因为有“洗餐巾”的条件,我们连接了晚点x与其他早点的边。但这会导致x向汇点的流量减少,自然无法成为最大流。于是考虑删去晚点向汇点的边,并用其他等效条件替代。
注意到所有晚点均仅由其对应的早点提供流量,而汇点的流量仅由所有晚点提供。那么不如断开晚点向汇点的边,改连早点向汇点连(r,0)的边。与此对应,需从源点向汇点连(r,0)的边。我们再考虑新网络能否与原问题建立一一对应关系。此时早点向汇点的边含义变为:第x天的所有干净餐巾为r条,不管之后的去向如何,但最终一定归于垃圾桶;源点向晚点的边含义为:第x天产生r条脏餐巾。这样的建模非常巧妙,实现了对应,且原问题的对应可行流一定是最大流。

Code

#include<cstdio>
#include<cstring>
#include<queue>
#define ll long long
using namespace std;
const int N = 4000 + 5;
const int M = 2e5 + 5;
const int INF = 0x3f3f3f3f;
const ll LINF = 0x3f3f3f3f3f3f3f3fll;
int n, s, t, r[N], p, t1, v1, t2, v2;
int head[N], nxt[M], ver[M], c[M], tot = 1;
int incf[N], pre[N], v[N];
ll w[M], dis[N];
void Add(int x, int y, int cap, int val) {
    nxt[++tot] = head[x]; head[x] = tot; ver[tot] = y; c[tot] = cap; w[tot] = val;
    nxt[++tot] = head[y]; head[y] = tot; ver[tot] = x; c[tot] = 0; w[tot] = -val;
}
bool Spfa() {
    memset(dis, 0x3f, sizeof (dis));
    queue<int> q;
    q.push(s); dis[s] = 0; incf[s] = INF; v[s] = 1;
    while (!q.empty()) {
        int x = q.front(); q.pop();
        v[x] = 0;
        for (ll i = head[x]; i; i = nxt[i]) {
            int y = ver[i];
            if (c[i] && dis[y] > dis[x] + w[i]) {
                dis[y] = dis[x] + w[i];
                incf[y] = min(c[i], incf[x]);
                pre[y] = i;
                if (!v[y]) {
                    v[y] = 1; q.push(y);
                }
            }
        }
    }
    return dis[t] != LINF;
}
int main() {
    scanf("%d", &n); s = 2 * n + 1; t = 2 * n + 2;
    for (int i = 1; i <= n; i++) scanf("%d", &r[i]);
    scanf("%d%d%d%d%d", &p, &t1, &v1, &t2, &v2);
    for (int i = 1; i <= n; i++) {
        Add(2 * i - 1, t, r[i], 0);
        Add(s, 2 * i, r[i], 0);
        Add(s, 2 * i - 1, INF, p);
        if (i + t1 <= n) Add(2 * i, 2 * (i + t1) - 1, INF, v1);
        if (i + t2 <= n) Add(2 * i, 2 * (i + t2) - 1, INF, v2);
        if (i < n) Add(2 * i - 1, 2 * i + 1, INF, 0);
    }
    ll cost = 0;
    while (Spfa()) {
        cost += incf[t] * dis[t];
        for (ll i = t; i != s; i = ver[pre[i] ^ 1]) {
            c[pre[i]] -= incf[t];
            c[pre[i] ^ 1] += incf[t];
        }
    }
    printf("%lld\n", cost);
    return 0;
}

学到一个小技巧:memset对long long使用时,0x3f表示0x3f3f3f3f3f3f3f3f,其两倍接近2631,相似于0x3f3f3f3f在int中的地位。
网络流问题注重建模。训练的方式为刷题+理解归纳。

本文作者:realFish的博客

本文链接:https://www.cnblogs.com/fish07/p/16014488.html

版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。

posted @   realFish  阅读(33)  评论(0编辑  收藏  举报
点击右上角即可分享
微信分享提示
评论
收藏
关注
推荐
深色
回顶
收起