1 条题解
-
0
C2. 调整演示(困难版本)详细题解
1. 问题理解
我们有 个成员,初始队伍顺序由排列 给出。有 张幻灯片,要求第 张幻灯片由成员 讲解。
操作规则:
- 每张幻灯片由当前队伍最前面的成员讲解
- 讲解完毕后,可以将该成员移动到队伍中的任意位置(其余成员相对顺序不变)
需要判断是否存在一种操作方式满足所有要求。
本题中 可以很大,我们需要在每次修改 中的某个值后快速判断。
2. 关键转化:重标号
首先,我们按照初始队伍顺序重新编号成员:
令初始队伍中第 个位置的成员编号为 。也就是说:
- 原数组 中的 变成成员
- 原数组 中的 变成成员
- ...
- 原数组 中的 变成成员
同时,将数组 中的所有成员编号按照这个映射进行转换。
结果:初始队伍顺序变成了 。
这样做的好处是简化了问题:我们不再需要关心 的具体值,只需要知道成员编号与初始位置的关系。
3. 无更新时的判断算法
让我们先考虑 的情况(简单版本)。
按顺序处理 ,同时维护:
current:当前队伍最前面的成员(初始为 )pending:一个集合,存放已经讲解过但还未确定最终位置的成员
处理规则:
当处理到 时:
情况1:
- 让该成员讲解当前幻灯片
- 讲解完毕后,他成为
pending成员(因为我们可以稍后把他放回队伍前面) - 更新
current为队伍中的下一个成员(即不在pending中的最小成员)
情况2: 在
pending中- 我们可以通过之前移动该成员时设定合适的目标位置,使得现在他正好在队伍最前面
- 讲解完毕后,他仍然是一个
pending成员 current保持不变
情况3:否则
- 无法让 讲解当前幻灯片 → 输出
"TIDAK"
算法正确性:
- 情况1:当前成员正好在最前面,直接讲解
- 情况2:该成员之前被移到了后面,但我们可以安排他在此时回到最前面
- 情况3:该成员既不在最前面,也没有被移动过,无法出现
4. 重要观察
从上述算法中,我们可以发现几个关键性质:
性质1:一旦一个成员成为
pending,他就永远在pending中。性质2:一个成员 第一次出现在 中的时候,就是他成为
pending的时刻。性质3:成员成为
pending的顺序必须与初始队伍顺序一致。为什么性质3成立?
因为初始队伍顺序是 。成员 在队伍最前面,所以如果他第一次出现时不是第一个,那么他之前一定已经被移到了后面,但那时他还没有出现过,矛盾。因此成员 必须是第一个成为
pending的成员。当成员 成为
pending后,队伍最前面变成了成员 。类似地,成员 必须是下一个成为pending的成员,否则成员 第一次出现时,成员 可能还在前面(如果还没有被移走),或者成员 跑到了前面(不可能,因为成员 在成员 后面)。因此,成员 必须在成员 第一次出现之前成为
pending。
5. 转化为条件判断
基于上述观察,我们定义:
$\text{first}[x] = \begin{cases} \text{最小的 } i \text{ 使得 } b_i = x, & \text{如果 } x \text{ 在 } b \text{ 中出现} \\ m+1, & \text{如果 } x \text{ 在 } b \text{ 中未出现} \end{cases}$
定理:幻灯片演示是“好的” $\text{first}[1] \le \text{first}[2] \le \dots \le \text{first}[n]$
证明:
必要性:
- 成员 必须在成员 第一次出现之前成为
pending - 成员 成为
pending的时刻就是 - 成员 第一次出现的时刻是
- 因此必须有
充分性:
- 如果 $\text{first}[1] \le \text{first}[2] \le \dots \le \text{first}[n]$,我们可以构造操作:
- 当遇到成员 第一次出现时,如果当前最前面不是 ,说明 已经在
pending中 - 我们可以通过之前移动 时的位置选择,让 在这个时刻出现在最前面
- 这样所有要求都可以满足
- 当遇到成员 第一次出现时,如果当前最前面不是 ,说明 已经在
因此,问题转化为维护 数组的非递减性。
6. 处理更新操作
现在考虑有 次更新(困难版本)。每次更新将 改为 。
我们需要在每次更新后快速判断 $\text{first}[1] \le \text{first}[2] \le \dots \le \text{first}[n]$ 是否成立。
6.1 数据结构设计
对于每个成员 (),我们维护一个有序集合 ,存储 在 中的所有出现位置。
那么: $\text{first}[x] = \begin{cases} \min(S_x), & \text{如果 } S_x \text{ 非空} \\ m+1, & \text{如果 } S_x \text{ 为空} \end{cases}$
6.2 维护非递减性
我们维护一个变量 ,表示满足 的 的数量()。
初始化:
- 遍历 数组,将每个位置 插入到 中
- 计算每个 的值
- 计算 的初始值
更新操作(将 从 改为 ):
-
从 中删除 :
- 更新前的 记为
- 删除后,新的 记为
- 如果 ,则需要更新与 相邻的比较
-
向 中插入 :
- 更新前的 记为
- 插入后,新的 记为
- 如果 ,则需要更新与 相邻的比较
-
更新 :
- 对于每个受影响的 ( 范围内的 ),先减去原来的比较结果(如果成立)
- 更新 值
- 再加上新的比较结果(如果成立)
注意需要处理边界情况( 或 时只有一个相邻比较)。
6.3 判断结果
每次更新后:
- 如果 ,则 数组非递减 → 输出
"YA" - 否则输出
"TIDAK"
7. 时间复杂度分析
- 预处理:(插入所有 中的元素到集合)
- 每次更新:(删除、插入、更新 和 )
- 总复杂度:
由于 的总和不超过 (所有测试用例),这个复杂度完全可以接受。
8. 实现细节
8.1 集合的选择
- 可以使用 C++ 的
std::set或std::multiset - 由于每个值可能出现多次,使用
std::set存储所有位置
8.2 获取最小值
- 对于
std::set,最小值是*S[x].begin() - 空集合时返回
8.3 更新 的辅助函数
auto update = [&](int x, int delta) { // 更新与 x 和 x+1 的比较 if (1 <= x && x < n) { // 比较 first[x] 和 first[x+1] if (first[x] <= first[x+1]) c += delta; } };
9. 完整算法流程
1. 读入 t 个测试用例 2. 对每个测试用例: a. 读入 n, m, q b. 读入数组 a,并建立映射(成员重编号) c. 读入数组 b,应用映射转换 d. 初始化:为每个成员 x 建立空集合 S[x] e. 遍历 b,将位置 i 插入 S[b_i] f. 计算 first[x](S[x] 的最小值或 m+1) g. 计算初始 c h. 输出初始状态的结果 i. 对于每次更新: - 读取 s, t - 将 t 映射为新编号 - old = b[s] - 从 S[old] 删除 s,更新 first[old] 和 c - 向 S[t] 插入 s,更新 first[t] 和 c - b[s] = t - 输出当前状态的结果
10. 示例验证
以题目中的第二个测试用例为例(经过重标号后):
- $\text{first}[1] = 1, \text{first}[2] = 3, \text{first}[3] = 4$
- 成立 →
"YA"
第三个测试用例:
- $\text{first}[1] = 2, \text{first}[2] = 4, \text{first}[3] = 1, \text{first}[4] = 6$
- 成立,但 不成立 →
"TIDAK"
与题目输出一致。
11. 总结
本题的核心思想是:
- 重标号简化初始顺序
- 发现等价条件:$\text{first}[1] \le \text{first}[2] \le \dots \le \text{first}[n]$
- 使用有序集合维护每个值的出现位置
- 维护相邻比较计数 ,实现 的更新
这样,我们就得到了一个高效的动态维护算法,能够处理困难版本中 很大的情况。
- 1
信息
- ID
- 6310
- 时间
- 1000ms
- 内存
- 256MiB
- 难度
- 2
- 标签
- 递交数
- 1
- 已通过
- 1
- 上传者