梦魔

用端点贪心把每个梦魔的限制转成嵌套区间,再用单调栈和计数排序在线性时间求出所有落点答案。

OJ: shumeng

题目 ID: CSP202412E

难度:提高+/省选-

标签:贪心单调栈前缀和区间最值

日期: 2026-07-31 16:21

形式化题目

nn 只梦魔排成一列,梦魔 ii 防御力为 aia_i,击杀后攻击力增加 bib_i。初始攻击力为 xx,从间隙 pp(第 pp 与第 p+1p+1 只之间)开始,每次只能击杀当前最左端或最右端的梦魔,且当前攻击力必须不小于其防御力。

对每个 p=1n1p = 1 \dots n-1,求能击杀全部梦魔所需的最小初始攻击力 f(p)f(p),输出所有 f(p)f(p) 的异或和。每次修改都从初始数组重新开始。

思路

朴素做法:递归选择端点

先看直接做法:固定落点后,每次递归选择击杀左端或右端,返回所需的最小初始攻击力。

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
// brute.cpp:小数据暴力解,把每次选择左端或右端看成递归分支,只适合小规模对拍。
#include <bits/stdc++.h>
using namespace std;

int n;
vector<long long> a;
vector<long long> b;

// 当前左右边界为 left、right(已击杀范围之外的第一个),返回所需的最小初始攻击力
long long dfs(int left, int right) {
    if (left == 0 && right == n + 1) return 0;

    long long answer = (1LL << 62);
    if (left > 0) {
        long long need_after = dfs(left - 1, right);
        long long need_now = max(a[left], need_after - b[left]);
        answer = min(answer, need_now);
    }
    if (right <= n) {
        long long need_after = dfs(left, right + 1);
        long long need_now = max(a[right], need_after - b[right]);
        answer = min(answer, need_now);
    }
    return answer;
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    cin >> n;
    vector<long long> base_a(n + 2);
    vector<long long> base_b(n + 2);
    a.assign(n + 2, 0);
    b.assign(n + 2, 0);

    for (int i = 1; i <= n; i++) cin >> base_a[i];
    for (int i = 1; i <= n; i++) cin >> base_b[i];

    int q;
    cin >> q;
    for (int query = 0; query < q; query++) {
        a = base_a;
        b = base_b;

        int changed;
        cin >> changed;
        for (int i = 0; i < changed; i++) {
            int position;
            cin >> position >> a[position] >> b[position];
        }

        // 对每个间隙 p 独立递归求解,再取异或
        long long answer = 0;
        for (int split = 1; split < n; split++) {
            answer ^= dfs(split, split + 1);
        }
        cout << answer << '\n';
    }

    return 0;
}

每个间隙都要枚举两端的所有击杀顺序,只适合小数据验证。

固定落点的贪心

固定间隙 pp 后,左侧梦魔只能按 p,p1,,1p, p-1, \dots, 1 的顺序出现,右侧只能按 p+1,p+2,,np+1, p+2, \dots, n 的顺序出现,任意时刻只有左右两个端点可选。

若两端防御力满足 xyx \le y,优先击杀 xx 不会变差:设某方案先击杀右端 yy 后再击杀左端 xx,把它改成先击杀 xx,再按原顺序击杀右侧那些梦魔,xx 的防御力不超过第一个右端点且提前获得 bxb_x,每一步攻击力都不更低,方案依然可行。

于是贪心策略确定:始终优先击杀防御力较小的端点。问题变成在贪心过程中,每一步对初始攻击力提出什么下界。

一个梦魔产生两个区间限制

用单调栈求每个位置两侧“最近的不小于自己”的边界:

L[j]=j 左侧最近的 a[L[j]]a[j] 的位置,不存在时为 0;L[j] = j \text{ 左侧最近的 } a[L[j]] \ge a[j] \text{ 的位置,不存在时为 } 0;
R[j]=j 右侧最近的 a[R[j]]a[j] 的位置,不存在时为 n+1R[j] = j \text{ 右侧最近的 } a[R[j]] \ge a[j] \text{ 的位置,不存在时为 } n+1。

LLRR 时单调栈只能弹出严格更小的防御力,相等的必须保留。

jj 位于落点右侧(p<jp < j),当 L[j]pj1L[j] \le p \le j-1 时,jj 左侧直到 L[j]L[j] 之间的梦魔防御力都严格小于 aja_j,贪心会先击杀它们,jj 才成为关键端点,因此产生限制:

落点区间 [max(1,L[j]), j1],限制值 aj(prefixb[j1]prefixb[L[j]])\text{落点区间 } [\max(1, L[j]),\ j-1], \qquad \text{限制值 } a_j - (\text{prefix}_b[j-1] - \text{prefix}_b[L[j]])。

jj 位于落点左侧(pjp \ge j),对称地得到:

落点区间 [j, min(n1,R[j]1)],限制值 aj(prefixb[R[j]1]prefixb[j])\text{落点区间 } [j,\ \min(n-1, R[j]-1)], \qquad \text{限制值 } a_j - (\text{prefix}_b[R[j]-1] - \text{prefix}_b[j])。

其中 prefixb[t]\text{prefix}_b[t]b1++btb_1 + \dots + b_t。区间里每个 pp 都必须满足该限制,所以 f(p)f(p) 是覆盖它的所有候选限制值的最大值。

嵌套区间扫描

这些候选区间不会交叉:任意两个区间要么相离、相等,要么一个包含另一个(否则交叉部分会同时违反“最近不小于自己”的定义)。把区间按 (left 升序,right 降序)(\text{left 升序}, \text{right 降序}) 排序,扫描 p=1n1p = 1 \dots n-1 时,已经开始但尚未结束的区间正好形成一条嵌套链:

text
外层区间 [1, 8]
    内层区间 [3, 6]
        更内层区间 [4, 5]

用栈维护这条链:进入新区间时,栈节点保存“从栈底到该节点的限制值最大值”;区间右端点小于当前 pp 时弹出。栈顶最大值就是当前 f(p)f(p)

区间端点都在 1n11 \dots n-1 内,用两次计数排序代替比较排序,整个询问只需 O(n)O(n)

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * create_at: 2026-07-31 16:21
 * update_at: 2026-08-17 22:39
 */
#include <bits/stdc++.h>
using namespace std;

class FastInput {
private:
    static const int BUFFER_SIZE = 1 << 20;
    int position;
    int length;
    char buffer[BUFFER_SIZE];

    char get_char() {
        if (position == length) {
            length = (int)fread(buffer, 1, BUFFER_SIZE, stdin);
            position = 0;
            if (length == 0) return 0;
        }
        return buffer[position++];
    }

public:
    FastInput() : position(0), length(0) {}

    long long read_long_long() {
        char current = get_char();
        while (current <= ' ' && current != 0) current = get_char();

        long long value = 0;
        while (current >= '0' && current <= '9') {
            value = value * 10 + current - '0';
            current = get_char();
        }
        return value;
    }
};

struct Interval {
    int left;
    int right;
    long long value;
};

struct StackNode {
    int right;
    long long maximum;
};

int n;
vector<long long> base_a;
vector<long long> base_b;
vector<long long> a;
vector<long long> b;
vector<long long> prefix_b;
vector<int> previous_ge;
vector<int> next_ge;
vector<Interval> intervals;
vector<int> order_by_right;
vector<int> order_by_left;
vector<int> counter;
vector<StackNode> interval_stack;

void build_nearest_greater() {
    vector<int> stack;
    stack.reserve(n);

    for (int i = 1; i <= n; i++) {
        while (!stack.empty() && a[stack.back()] < a[i]) {
            stack.pop_back();
        }
        if (stack.empty()) previous_ge[i] = 0;
        else previous_ge[i] = stack.back();
        stack.push_back(i);
    }

    stack.clear();
    for (int i = n; i >= 1; i--) {
        while (!stack.empty() && a[stack.back()] < a[i]) {
            stack.pop_back();
        }
        if (stack.empty()) next_ge[i] = n + 1;
        else next_ge[i] = stack.back();
        stack.push_back(i);
    }
}

void build_intervals() {
    intervals.clear();
    intervals.reserve(2 * n);

    for (int i = 1; i <= n; i++) {
        int left = previous_ge[i];
        int right = i - 1;
        if (left < 1) left = 1;
        if (left <= right) {
            long long value = a[i] - (prefix_b[i - 1] - prefix_b[previous_ge[i]]);
            Interval current;
            current.left = left;
            current.right = right;
            current.value = value;
            intervals.push_back(current);
        }

        left = i;
        right = next_ge[i] - 1;
        if (right > n - 1) right = n - 1;
        if (left <= right) {
            long long value = a[i] - (prefix_b[next_ge[i] - 1] - prefix_b[i]);
            Interval current;
            current.left = left;
            current.right = right;
            current.value = value;
            intervals.push_back(current);
        }
    }
}

void sort_intervals() {
    int interval_count = (int)intervals.size();
    order_by_right.resize(interval_count);
    order_by_left.resize(interval_count);

    fill(counter.begin(), counter.end(), 0);
    for (int i = 0; i < interval_count; i++) {
        counter[intervals[i].right]++;
    }

    int position = 0;
    for (int right = n - 1; right >= 1; right--) {
        int count = counter[right];
        counter[right] = position;
        position += count;
    }
    for (int i = 0; i < interval_count; i++) {
        int right = intervals[i].right;
        order_by_right[counter[right]++] = i;
    }

    fill(counter.begin(), counter.end(), 0);
    for (int i = 0; i < interval_count; i++) {
        counter[intervals[order_by_right[i]].left]++;
    }

    position = 0;
    for (int left = 1; left <= n - 1; left++) {
        int count = counter[left];
        counter[left] = position;
        position += count;
    }
    for (int i = 0; i < interval_count; i++) {
        int id = order_by_right[i];
        int left = intervals[id].left;
        order_by_left[counter[left]++] = id;
    }
}

long long calculate_answer() {
    prefix_b[0] = 0;
    for (int i = 1; i <= n; i++) {
        prefix_b[i] = prefix_b[i - 1] + b[i];
    }

    build_nearest_greater();
    build_intervals();
    sort_intervals();

    interval_stack.clear();
    interval_stack.reserve(intervals.size());
    long long answer = 0;
    int interval_count = (int)intervals.size();
    int current_interval = 0;

    for (int split = 1; split <= n - 1; split++) {
        while (!interval_stack.empty() && interval_stack.back().right < split) {
            interval_stack.pop_back();
        }

        while (current_interval < interval_count) {
            int id = order_by_left[current_interval];
            if (intervals[id].left != split) break;

            long long maximum = intervals[id].value;
            if (!interval_stack.empty() && interval_stack.back().maximum > maximum) {
                maximum = interval_stack.back().maximum;
            }
            StackNode node;
            node.right = intervals[id].right;
            node.maximum = maximum;
            interval_stack.push_back(node);
            current_interval++;
        }

        long long value = interval_stack.back().maximum;
        answer ^= value;
    }
    return answer;
}

int main() {
    FastInput input;

    n = (int)input.read_long_long();
    base_a.assign(n + 2, 0);
    base_b.assign(n + 2, 0);
    a.assign(n + 2, 0);
    b.assign(n + 2, 0);
    prefix_b.assign(n + 2, 0);
    previous_ge.assign(n + 2, 0);
    next_ge.assign(n + 2, n + 1);
    counter.assign(n + 2, 0);

    for (int i = 1; i <= n; i++) base_a[i] = input.read_long_long();
    for (int i = 1; i <= n; i++) base_b[i] = input.read_long_long();

    int q = (int)input.read_long_long();
    for (int query = 0; query < q; query++) {
        a = base_a;
        b = base_b;

        int changed = (int)input.read_long_long();
        for (int i = 0; i < changed; i++) {
            int position = (int)input.read_long_long();
            a[position] = input.read_long_long();
            b[position] = input.read_long_long();
        }

        long long answer = calculate_answer();
        printf("%lld\n", answer);
    }

    return 0;
}

复杂度

  • 时间:每次询问复制初始数组并应用 kk 个修改后,完成前缀和、两次单调栈、候选区间生成、两次计数排序与区间栈扫描,O(n+k)O(n + k)
  • 空间:O(n)O(n)

总结

核心是把“左右端点交替击杀”转化为贪心过程。一个梦魔只有在两侧更小防御力都被清掉后才成为关键端点,于是它的限制自然对应一个落点区间;最近的不小于自身元素给出区间边界。所有区间具有嵌套结构,最后用栈维护区间最大值即可。

图示解析

这张图串起一次询问从数组到异或答案的主线:

text
防御力 a、收益 b
|- 单调栈求每个位置两侧最近的 >= 边界 L、R
   `- 每个梦魔生成至多两个落点限制区间
      |- 区间只能相离或嵌套
      `- 扫描落点,用栈维护覆盖区间的最大限制
         `- 异或所有 f(p)

先由 LLRR 判断一个梦魔在哪些间隙会成为关键端点,再顺着区间的包含关系观察栈为何只需要保存一条活动链。每个候选区间只入栈、出栈一次,扫描阶段保持线性。