洛谷 P1251餐巾计划问题 题解

题面

传送门

描述

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

输入格式

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

输出格式

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

题解

不难看出,这是一道最小费用流的题目。此类问题建模时,一定要注意“最小费用基于最大流”,要使问题的最终答案在网络中一定对应一个最大流才行。否则辛苦打代码,等到输出时才发现建模错误,那会浪费不少时间。(亲身经历)
首先,将每天看做一个点,那么流入这个点的流量不能小于\(r\)。题面中的“每天结束时”也启发我们用点边转化的方式,即拆点,处理此题。
不难想到一个看似正确的做法:将每个点拆成“早上的点”与“晚上的点”,并从早点向晚点连\((r,0)\)(容量为r,费用为0)的边。设一个虚拟源点\(S\),从\(S\)向所有早点连\((+∞,p)\)的边,表示每天早上可以买任意条餐巾。设虚拟汇点\(T\)(垃圾桶),从所有晚点向\(T\)\((+∞,0)\)的边,表示脏餐巾可以任意丢弃。在从每个第\(x\)天的晚点向第\(x+m\)天的早点连\((r_x,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,其两倍接近\(2^{63}-1\),相似于0x3f3f3f3f在int中的地位。
网络流问题注重建模。训练的方式为刷题+理解归纳。

posted @ 2022-03-16 21:04  realFish  阅读(39)  评论(0)    收藏  举报