用端点贪心把每个梦魔的限制转成嵌套区间,再用单调栈和计数排序在线性时间求出所有落点答案。
OJ: shumeng
题目 ID: CSP202412E
难度:提高+/省选-
标签:贪心单调栈前缀和区间最值
日期: 2026-07-31 16:21
形式化题目
对每个
思路
朴素做法:递归选择端点
先看直接做法:固定落点后,每次递归选择击杀左端或右端,返回所需的最小初始攻击力。
/**
* 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;
}每个间隙都要枚举两端的所有击杀顺序,只适合小数据验证。
固定落点的贪心
固定间隙
若两端防御力满足
于是贪心策略确定:始终优先击杀防御力较小的端点。问题变成在贪心过程中,每一步对初始攻击力提出什么下界。
一个梦魔产生两个区间限制
用单调栈求每个位置两侧“最近的不小于自己”的边界:
求
若
若
其中
嵌套区间扫描
这些候选区间不会交叉:任意两个区间要么相离、相等,要么一个包含另一个(否则交叉部分会同时违反“最近不小于自己”的定义)。把区间按
外层区间 [1, 8]
内层区间 [3, 6]
更内层区间 [4, 5]用栈维护这条链:进入新区间时,栈节点保存“从栈底到该节点的限制值最大值”;区间右端点小于当前
区间端点都在
代码
/**
* 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;
}复杂度
- 时间:每次询问复制初始数组并应用
个修改后,完成前缀和、两次单调栈、候选区间生成、两次计数排序与区间栈扫描, 。 - 空间:
。
总结
核心是把“左右端点交替击杀”转化为贪心过程。一个梦魔只有在两侧更小防御力都被清掉后才成为关键端点,于是它的限制自然对应一个落点区间;最近的不小于自身元素给出区间边界。所有区间具有嵌套结构,最后用栈维护区间最大值即可。
图示解析
这张图串起一次询问从数组到异或答案的主线:
防御力 a、收益 b
|- 单调栈求每个位置两侧最近的 >= 边界 L、R
`- 每个梦魔生成至多两个落点限制区间
|- 区间只能相离或嵌套
`- 扫描落点,用栈维护覆盖区间的最大限制
`- 异或所有 f(p)先由