1 条题解
-
0
1. 理解删空过程
删空规则:
当前数列长度为 ,就删除所有等于 的数,然后长度变为 (剩余数字个数),继续删除等于 的数,直到空。例如:
- ,删除所有 →
- ,删除所有 →
- ,删除所有 → 空
可以删空。
2. 可删空的充要条件
我们考虑一个长度为 的数列 可删空的条件。
设 表示数值 在数列中出现的次数。
删空过程模拟:
- 初始长度 ,删除所有等于 的数,剩余长度
- 接着删除所有等于 的数,剩余长度
- 依此类推。
如果过程中某一步 在数列中不存在(),就无法继续删除,除非我们提前修改一些数字。
2.1 关键观察
设 = 在删除过程中,当需要删除数值 时,这个数值在数列中出现的次数(包括之前修改过的)。
如果 ,这一步就能继续。
如果 ,我们就必须提前修改某个数,使其变成 。因此,最少修改次数 = 删除过程中遇到的 的 的个数。
但 是动态的,因为整体加减会影响数值。
3. 整体偏移的处理
设整体加的总和为 (可以为负)。
初始 ,每次 则 , 则 。那么实际数值 在比较时,应该用 吗?
不对,因为删除规则中的 是当前长度,不受整体加减影响。但数列中的数字受整体加减影响。更准确地说:
设 是原始输入调整后的值(这样整体加减时 不变)。
但单点修改时,修改的是 而不是 ,所以 仍然成立。那么比较时:
当前长度为 ,我们要看是否存在 的数,即 。因此,定义 表示 的个数( 范围可能很大,因为 会变)。
4. 转化为 数组问题
设 (整体加减的偏移量)。
删除过程:
- 初始
- 若 ,则
- 否则,必须修改一次,(因为我们修改一个数变成 )
所以问题变成:
给定 数组和 ,模拟删除过程,统计 的次数。
5. 快速计算答案
直接模拟每次删除过程是 的,总复杂度 不可行。
我们需要快速计算:
其中 是删除过程中访问到的 的集合。
5.1 删除路径的规律
删除过程是确定的:
因为如果 ,就减 ,否则减 1(修改一次)。注意 时,。
时,。
5.2 关键优化
设 。
当 时, 会减少 1,然后 也减少 1(因为 减 1, 不变)。
所以一旦遇到一个 ,就会连续递减直到遇到 。因此,连续的 满足 的段 会对应一连串的修改。
5.3 转化为求第一个非零位置
我们从 开始,。
如果 ,则 跳到 , 变成 。
如果 ,则这一段连续的 都会导致修改,直到 。因此,问题变成:
从 开始,每次遇到 ,就沿着 递减的方向走,直到遇到 ,然后跳过一段。
5.4 数据结构维护
我们需要快速查询:
给定 ,求最小的 使得 ,且知道 之后可以跳到 继续。这相当于在 数组上做“下一个非零位置”查询,并且支持单点更新(单点修改影响 )。
可以用并查集或链表维护每个位置的下一个非零位置。
具体地,维护 表示 左边(包括 )最近的非零 的位置。
当 从 0 变成 1 时,合并区间;当 从 1 变成 0 时,拆分区间。
6. 算法步骤
- 初始化 ,(因为初始 )。
- 初始化 数组(用字典或数组,范围 左右)。
- 用并查集维护 指针: 指向 左边最近的 的位置,如果没有则为 。
- 对于每次修改:
- 如果是整体加减,更新 。
- 如果是单点修改,更新 和并查集。
- 计算答案:
从 开始,- 如果 ,则 ,继续。
- 如果 ,则找到 ,如果 不存在( 太小),则从 到 都要修改,即 次修改;否则从 到 这一段都是 0,修改次数 ,然后 。
重复直到 ,总修改次数就是答案。
7. 复杂度
- 每次查询答案的步数 ≤ 修改次数 + 1,因为每次跳跃至少减少 的 ,并且遇到零段时直接跳到下一个非零位置。
- 并查集操作近似 。
- 总复杂度 。
8. 代码框架(C++)
#include <bits/stdc++.h> using namespace std; const int MAX = 900005; // 范围 [-300000, 600000] 偏移 300000 const int OFFSET = 300000; int n, m; int delta; int cnt[MAX]; int b[150005]; int parent[MAX]; int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); return parent[x]; } void unite(int x, int y) { x = find(x), y = find(y); if (x != y) parent[x] = y; } void add_val(int v) { v += OFFSET; cnt[v]++; if (cnt[v] == 1) { if (cnt[v-1] >= 1) unite(v-1, v); if (cnt[v+1] >= 1) unite(v, v+1); } } void remove_val(int v) { v += OFFSET; cnt[v]--; if (cnt[v] == 0) { parent[v] = v; } } int query_next(int v) { v += OFFSET; if (cnt[v] >= 1) return v - OFFSET; int p = find(v); if (p >= v) return -1e9; // 没有左边的非零 return p - OFFSET; } int solve() { int start = n - delta; int ans = 0; while (start > 0) { int need = start; if (cnt[need + OFFSET] >= 1) { start -= cnt[need + OFFSET]; } else { int p = query_next(need); if (p < -1e8) { ans += start; break; } ans += (need - p); start = p - cnt[p + OFFSET]; } } return ans; } int main() { // 初始化 parent for (int i = 0; i < MAX; i++) parent[i] = i; scanf("%d%d", &n, &m); delta = 0; for (int i = 1; i <= n; i++) { int x; scanf("%d", &x); b[i] = x; add_val(x - delta); // 初始 delta=0 } for (int i = 0; i < m; i++) { int p, x; scanf("%d%d", &p, &x); if (p == 0) { // 整体加减 if (x == 1) { delta++; } else { delta--; } } else { // 单点修改 remove_val(b[p] - delta); b[p] = x; add_val(b[p] - delta); } printf("%d\n", solve()); } return 0; }
9. 总结
这道题的核心是将删空过程转化为对 数组的跳跃查询,利用并查集维护连续零段的快速跳过,从而高效回答多次询问。
- 1
信息
- ID
- 4924
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 10
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者