澳八机器人 P5324 [BJOI2019] 删数 题解
作者:admin | 分类:澳八机器人 | 浏览:3 | 日期:2026年09月05日结合你之前关注的图论算法题解、工业级线段树工程化落地的相关背景,这道题的核心难点不是复杂的
算法推导,而是跳出常规动态规划的思路,通过贪心结论把问题转化为线段树可维护的区间统计问题,
同时巧妙处理整体加减操作,最终实现O(mlog n)的全量操作复杂度。
核心题意重述
给定一个长度为n的数列,支持单点修改、数列整体加x、数列整体减x三种操作,每次操作后需要回答:
至少修改几个数,可以让数列满足“有限次删空”的规则。删数操作的定义是:当前数列长度为k时,一
次性删掉所有等于k的数,重复操作直到数列清空。
关键贪心结论推导
这道题最核心的突破口,是跳出动态规划的复杂思路,直接推导出极简的贪心结论:
数字的顺序完全不影响最终能否删空,只需要统计每个数值的出现次数即可。
对于数值为i的数,假设有cnt[i]个,那么这些数可以覆盖区间 [i - cnt[i] + 1, i]。
最终数列可以被删空的充要条件是:区间[1, n]内的每一个位置,都至少被一个上述区间覆盖。
最少需要修改的次数,就等于区间[1, n]内没有被覆盖的位置的总数量。
这个结论的正确性可以直接验证:每一个未被覆盖的位置,只需要修改一个数,就能让它覆盖到这个空位,
一次操作就能补上一个缺口,这是理论上的最小修改次数,不可能存在更优的方案。
数据结构优化与整体加减处理
如果只有单点修改,我们直接维护每个位置的覆盖次数,统计区间内0的个数就能得到答案,但题目中的整
体加减操作如果直接暴力修改数组,时间复杂度完全无法接受,这里用“偏移量平移查询区间”的经典技巧
巧妙处理:
我们不直接修改存储数值的桶数组,而是维护一个全局偏移量delta。当执行整体加1操作时,delta直接加1;
整体减1操作时,delta直接减1,完全不需要遍历修改所有元素。
原本我们需要查询的有效区间是[1, n],随着delta的变化,我们把查询区间同步平移:当delta增加1,查询区
间整体左移一位;delta减少1,查询区间整体右移一位,用查询区间的移动替代全量数组修改。
用线段树维护每个位置的覆盖次数,线段树支持区间加减、查询区间内0的总个数。由于覆盖次数永远是非负整
数,我们只需要维护区间最小值和最小值的出现次数,当区间最小值为0时,对应的个数就是未被覆盖的位置总
数,也就是我们要求的答案。
边界与空间处理
由于整体加减操作可能让数值落到负数区间,我们需要把线段树的总空间开为3*n或者2n+2m的大小,把初始
原点偏移到中间位置,避免数组下标越界,完全覆盖所有可能的数值范围。
核心代码框架
cpp
#include <bits/stdc++.h>
using namespace std;
const int N = 150000 + 5, M = 3 * N;
int a[N], b[M], mn[M << 2], cnt[M << 2], tag[M << 2];
// 线段树基础操作:上传、下推、区间修改、区间查询0的个数
void push_up(int p) {
mn[p] = min(mn[p<<1], mn[p<<1|1]);
cnt[p] = 0;
if(mn[p<<1] <= mn[p<<1|1]) cnt[p] += cnt[p<<1];
if(mn[p<<1] >= mn[p<<1|1]) cnt[p] += cnt[p<<1|1];
}
void add_tag(int p, int k) {
tag[p] += k; mn[p] += k;
}
void push_down(int p) {
if(!tag[p]) return;
add_tag(p<<1, tag[p]); add_tag(p<<1|1, tag[p]);
tag[p] = 0;
}
void build(int p, int l, int r) {
if(l == r) { cnt[p] = 1; return; }
int mid = l + r >> 1;
build(p<<1, l, mid); build(p<<1|1, mid+1, r);
push_up(p);
}
void range_add(int p, int l, int r, int L, int R, int x) {
if(l > R || r < L) return;
if(l >= L && r <= R) { add_tag(p, x); return; }
push_down(p);
int mid = l + r >> 1;
range_add(p<<1, l, mid, L, R, x);
range_add(p<<1|1, mid+1, r, L, R, x);
push_up(p);
}
int query_zero(int p, int l, int r, int L, int R) {
if(l > R || r < L) return 0;
if(l >= L && r <= R) return (mn[p] == 0) * cnt[p];
push_down(p);
int mid = l + r >> 1;
return query_zero(p<<1, l, mid, L, R) + query_zero(p<<1|1, mid+1, r, L, R);
}
所有单点修改、整体加减操作都可以通过调用线段树的区间加减接口完成,每次操作后直接查询平移后的[L, R]区
间内0的个数,就是当前问题的答案。
需要我为你整理这道题的完整可提交AC代码+分步注释版本吗?直接复制就能在OJ平台通过所有测试点。