[Luogu 4217] 产品销售 题解

产品销售

又是很好的线段树题.

机房的机器, 为什么我在别的地方输的都是正常的全角字符, 到了 CSDN 就变成了只能输出半角字符…

大致思路

不是很直接的贪心题, 应当先对题目进行抽象建模.

  • S → i S \rightarrow i Si 连边 ( D i , 0 ) (D_i, 0) (Di,0), 第 i i i 天有 D i D_i Di 个购买需求.
  • i → T i \rightarrow T iT 连边 U i , P i U_i, P_i Ui,Pi, 第 i i i 天的生产能力.
  • 然后是这一天需求的可以在之前的时间点生产, 也可能在之后的时间点生产.
  • i + 1 → i i+1 \rightarrow i i+1i 连边 M i M_i Mi, i → i + 1 i \rightarrow i+1 ii+1 连边 C i C_i Ci.

注意到处理 D i D_i Di 的顺序并不影响最终答案, 所以可以考虑从左往右一次处理.

i i i 天的购买需求显然必然会被全部处理. 它可能在之前, 此刻, 之后被处理.

注意到, 由于权值为正, 如果从 i i i 流到 i − 1 i-1 i1 再流回 i i i, 肯定不优.

根据以上处理方式, i → i − 1 i \rightarrow i-1 ii1 的边不可能被被退流, i → i + 1 i \rightarrow i+1 ii+1 的边可能会被退流.

i i i 流到 i + 1 i+1 i+1, 它的代价一定是 C i × f l o w C_i \times flow Ci×flow. 而从 i + 1 i+1 i+1 流到 i i i, 因为退流的代价为 − C i × f l o w < 0 -C_i \times flow < 0 Ci×flow<0, 而普通的代价为 M i > 0 M_i > 0 Mi>0, 如果能够退流, 优先退流; 不能退流时, 才会走 M i M_i Mi.

设当前这一部分流量 f l o w flow flow 流入汇点之前到达的节点为 p p p, 即通过 p → T p \rightarrow T pT 流入 T T T.

  • 向左走, 如果有反悔边可走, 必然走反悔边, 因为反悔边代价为负.
  • 反悔边的流量在处理完它的左端点的时候, 已经不可能继续增加了.
  • 而消耗反悔边, 又是从处理到它的右端点的时候开始的.
  • 考虑维护反悔边还剩多少.
  • 如果 p < i p < i p<i, 那么 [ p , i − 1 ] [p, i-1] [p,i1] 的所有反悔边的流量都要减少本轮的流量 f l o w flow flow.
  • 那么流量在这一轮被减到 0 0 0 的所有位置需要被找出来, 然后 “身份转变”, 由反悔边变成普通边.
  • 向右走的话, 那么 [ i , p − 1 ] [i, p-1] [i,p1] 的反悔边的流量加上 f l o w flow flow.

注意: 这里边的编号是它的左端点的编号.

线段树

具体来说, 需要建两只线段树, 而且两只维护的东西不同. (代码量一下子上来了.)

第一只线段树

第一个线段树的区间是 [ 1 , n ] [1, n] [1,n], 维护点的信息. 第 p p p 个位置的值是以当前第 i i i 天的 一个 需求被放到第 p p p 天解决的花费, 即 S → i → ⋯ → p → T S \rightarrow i \rightarrow \cdots \rightarrow p \rightarrow T SipT 的花费.
在第 i i i 天的 D i D_i Di 个需求全部解决之后, 考虑怎么将线段树的信息转移为第 i + 1 i+1 i+1 天的信息.

  • 对于 [ 1 , i ] [1, i] [1,i], 如果存在反悔 i → i + 1 i \rightarrow i+1 ii+1 的边, 那么区间加 − C i -C_i Ci, 否则区间加 M i M_i Mi.
  • 对于 [ i + 1 , n ] [i+1, n] [i+1,n], 撤掉 i → i + 1 i \rightarrow i+1 ii+1 的代价, 区间加 − C i -C_i Ci.

第一个线段树还需要支持查找全局最小值, 以及全局最小值的位置. 计算单次使用的流量需要知道位置.

考虑到一个点作为出口, 出口这一部分也有流量限制. 这里满流的时候, 直接给代价赋值 + ∞ +\infty +.

综上所述, 第一个线段树需要支持的操作: 区间加, 单点赋值 + ∞ +\infty +, 查询全局最小值&位置.

第二只线段树

第二个线段树的区间范围是 [ 1 , n − 1 ] [1, n-1] [1,n1], 维护边的信息.

它需要区间非 0 0 0 数最小值, 区间加正整数, 对非 0 0 0 值区间减正整数, 在区间减正整数之后需要找到新被变成 0 0 0 的位置, 然后把它由反悔边变成普通边.

零和非零还不一样… 一看就不是很好处理的样子…

不要慌. 线段树上除了非 0 0 0 数最小值之外, 还需要维护区间有没有 0 0 0, 区间有没有非 0 0 0.

这样就方便了.

新被变成 0 0 0 的那些区间, 先不改动零和非零的标记, 在寻找新变成 0 0 0 的位置的时候在修改它的标记. 不然的话, 一是没法知道区间还有没有非零 (这个最好需要通过 pushup 来获知), 二是不知道可能具体是那些位置. 而因为它变成 0 0 0 之后就不会再被处理了, 所以时间复杂度正确.

这个找位置的方法比较经典, 但是这里没有必要传 vector 的引用, 直接找到就对另一个线段树做区间修改就好了.

代码

说不明白啊, 看代码算了.

这题一发交到最优解.

#include <cstdio>
#include <cstring>
#include <vector>
#include <queue>
#include <algorithm>
#include <random>
#include <utility>
#define fi first
#define se second

using namespace std;
using LL = long long;
using LLL = __int128;
using pii = pair<int, int>;
const int MAXN = 1e5+5;
const int INF = 0x3f3f3f3f; 
mt19937 rnd(random_device{}());

template<typename Tp> void read(Tp &res) {
    char ch; bool op = 0; res = 0;
    do ch = getchar(), op |= ch == '-'; while (ch < '0' || ch > '9');
    do res = (res<<3)+(res<<1)+ch-48, ch = getchar(); while (ch>='0' && ch<='9');
    if (op) res = -res;
}

// I need two segment trees.

int n, inlim[MAXN], outlim[MAXN], ext[MAXN], lv[MAXN], rv[MAXN];

namespace SGT1 {
    struct Node {
        pii val;
        int tag;
        Node(){}
        Node(pii v) { val = v, tag = 0; }
        void modify(int x) {
            if (val.fi != INF) val.fi += x;
            tag += x;
        }
        Node operator+(Node x) {
            return Node(min(val, x.val));
        }
    } s[MAXN<<2];
    int ori[MAXN];
    void build(int cur, int l, int r) {
        if (l == r) return s[cur] = Node(make_pair(ori[l], l)), void();
        int mid = (l + r) >> 1;
        build(cur<<1, l, mid), build(cur<<1|1, mid+1, r);
        s[cur] = s[cur<<1] + s[cur<<1|1];
    }
    void init() {
        int s = 0;
        for (int i = 1; i <= n; ++i) {
            ori[i] = ext[i] + s;
            s += rv[i];
        }
        build(1, 1, n);
    }
    void spread(int cur) {
        if (!s[cur].tag) return;
        s[cur<<1].modify(s[cur].tag), s[cur<<1|1].modify(s[cur].tag);
        s[cur].tag = 0;
    }
    void update(int cur, int l, int r, int L, int R, int v) {
        if (L <= l && R >= r) return s[cur].modify(v);
        spread(cur);
        int mid = (l + r) >> 1;
        if (L <= mid) update(cur<<1, l, mid, L, R, v);
        if (R > mid) update(cur<<1|1, mid+1, r, L, R, v);
        s[cur] = s[cur<<1] + s[cur<<1|1];
    }
    void fullflow(int cur, int l, int r, int p) {
        if (l == r) return s[cur].val.fi = INF, void();
        spread(cur);
        int mid = (l + r) >> 1;
        if (p <= mid) fullflow(cur<<1, l, mid, p);
        else fullflow(cur<<1|1, mid+1, r, p);
        s[cur] = s[cur<<1] + s[cur<<1|1];
    }
}

namespace SGT2 {
    struct Node {
        int mn, tag;
        bool f0, f1;
        void modify(int x) {
            tag += x;
            if (x > 0 && f0) mn = x, f0 = 0, f1 = 1;
            else if (f1) mn += x;
            else mn = INF; 
        }
        Node operator+(Node x) {
            Node res;
            res.mn = min(mn, x.mn), res.tag = 0;
            res.f0 = f0 | x.f0, res.f1 = f1 | x.f1;
            return res;
        }
    } s[MAXN<<2];
    void build(int cur, int l, int r) {
        if (l == r) return s[cur] = {INF, 0, 1, 0}, void();
        int mid = (l + r) >> 1;
        build(cur<<1, l, mid), build(cur<<1|1, mid+1, r);
        s[cur] = s[cur<<1] + s[cur<<1|1];
    }
    void spread(int cur) {
        if (!s[cur].tag) return;
        s[cur<<1].modify(s[cur].tag), s[cur<<1|1].modify(s[cur].tag);
        s[cur].tag = 0;
    }
    void update(int cur, int l, int r, int L, int R, int v) {
        if (L <= l && R >= r) return s[cur].modify(v), void();
        spread(cur);
        int mid = (l + r) >> 1;
        if (L <= mid) update(cur<<1, l, mid, L, R, v);
        if (R > mid) update(cur<<1|1, mid+1, r, L, R, v);
        s[cur] = s[cur<<1] + s[cur<<1|1]; 
    }
    Node query(int cur, int l, int r, int L, int R) {
        if (L <= l && R >= r) return s[cur];
        spread(cur);
        int mid = (l + r) >> 1;
        if (R <= mid) return query(cur<<1, l, mid, L, R);
        if (L > mid) return query(cur<<1|1, mid+1, r, L, R);
        return query(cur<<1, l, mid, L, R) + query(cur<<1|1, mid+1, r, L, R);
    }
    void fullflow(int cur, int l, int r, int L, int R) {
        if (l == r) {
            s[cur] = {INF, 0, 1, 0};
            SGT1::update(1, 1, n, 1, l, lv[l] + rv[l]);
            return;
        } 
        spread(cur);
        int mid = (l + r) >> 1;
        if (L <= mid && !s[cur<<1].mn) fullflow(cur<<1, l, mid, L, R);
        if (R > mid && !s[cur<<1|1].mn) fullflow(cur<<1|1, mid+1, r, L, R);
        s[cur] = s[cur<<1] + s[cur<<1|1];
    }
}


int main() {
    #ifndef ONLINE_JUDGE
    freopen("lg4217.in", "r", stdin);
    freopen("lg4217.out", "w", stdout);
    #endif
    read(n);
    for (int i = 1; i <= n; ++i) read(inlim[i]);
    for (int i = 1; i <= n; ++i) read(outlim[i]);
    for (int i = 1; i <= n; ++i) read(ext[i]);
    for (int i = 1; i < n; ++i) read(lv[i]);
    for (int i = 1; i < n; ++i) read(rv[i]);
    SGT1::init(), SGT2::build(1, 1, n-1);
    LL ans = 0;
    for (int i = 1; i <= n; ++i) {
        while (inlim[i]) {
            int val = SGT1::s[1].val.fi, p = SGT1::s[1].val.se;
            if (p < i) {
                auto res = SGT2::query(1, 1, n-1, p, i-1);
                int cur = min(min(inlim[i], outlim[p]), res.mn);
                inlim[i] -= cur, outlim[p] -= cur, ans += 1ll * val * cur;
                SGT2::update(1, 1, n-1, p, i-1, -cur);
                SGT2::fullflow(1, 1, n-1, p, i-1);
            } else {
                int cur = min(inlim[i], outlim[p]);
                inlim[i] -= cur, outlim[p] -= cur, ans += 1ll * val * cur;
                if (p > i) SGT2::update(1, 1, n-1, i, p-1, cur);
            }
            if (!outlim[p]) SGT1::fullflow(1, 1, n, p);
        }
        if (i == n) break;
        SGT1::update(1, 1, n, 1, i, SGT2::query(1, 1, n-1, i, i).f1 ? -rv[i] : lv[i]);
        SGT1::update(1, 1, n, i+1, n, -rv[i]);
    }
    printf("%lld\n", ans);
    return 0;
}
题目描述似乎缺失了关键信息,通常我会需要了解“P10780 食物”是什么具体的算法竞赛题目,它来自在线平台洛谷(Luogu),以及该题目的大致背景、条件和目标。洛谷食物(Food)可能是某种数据结构算法问题,比如贪吃蛇、分配任务等。 然而,我可以给你提供一个通用的模板: **[洛谷 P10780 食物 - 题目解析]** 题目名称:P10780 食物(假设是关于食物分配或者饥饿游戏的问题) 链接:[插入实际题目链接] **背景:** 此题通常涉及动态规划或者搜索策略。场景可能是有n个参与者(选手或角色),每个都有特定的食物需求或者优先级,我们需要在有限的食物资源下合理分配。 **分析:** 1. **输入理解**:首先读入n个参与者的信息,包括每个人的需求量或优先级。 2. **状态定义**:可以定义dp[i][j]表示前i个人分配完成后剩余的食物能满足第j个人的最大程度。 3. **状态转移**:递推式可能涉及到选择当前人分配最多食物的版本,然后更新剩余的食物数。 4. **边界条件**:如果剩余食物不足以满足某人的需求,则考虑无法分配给他;如果没有食物,状态值设为0。 5. **优化策略**:可能需要对状态数组进行滚动更新,以减少空间复杂度。 **代码示例(伪代码或部分关键代码片段):** ```python # 假设函数分配_food(demand, remaining)计算分配给一个人后剩余的食物 def solve(foods): dp = [[0 for _ in range(max_demand + 1)] for _ in range(n)] dp = foods[:] # 从第一个到最后一个参与者处理 for i in range(1, n): for j in range(1, max_demand + 1): if dp[i-1][j] > 0: dp[i][j] = max(dp[i][j], dp[i-1][j] - foods[i]) dp[i][j] = max(dp[i][j], distribute_food_to(i, dp[i-1][j])) return dp[n-1][max_demand] ``` **相关问题--:** 1. 这道题是如何运用动态规划的? 2. 如果有优先级限制,应该如何调整代码? 3. 怎样设计搜索策略来解决类似问题?
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值