1 条题解

  • 0
    @ 2025-11-4 9:12:18

    1. 理解删空过程

    删空规则
    当前数列长度为 kk,就删除所有等于 kk 的数,然后长度变为 kk'(剩余数字个数),继续删除等于 kk' 的数,直到空。

    例如:(1,2,3)(1,2,3)

    • k=3k=3,删除所有 33(1,2)(1,2)
    • k=2k=2,删除所有 22(1)(1)
    • k=1k=1,删除所有 11 → 空
      可以删空。

    2. 可删空的充要条件

    我们考虑一个长度为 nn 的数列 aa 可删空的条件。

    c[i]c[i] 表示数值 ii 在数列中出现的次数。

    删空过程模拟:

    • 初始长度 nn,删除所有等于 nn 的数,剩余长度 n=nc[n]n' = n - c[n]
    • 接着删除所有等于 nn' 的数,剩余长度 n=nc[n]n'' = n' - c[n']
    • 依此类推。

    如果过程中某一步 kk 在数列中不存在(c[k]=0c[k] = 0),就无法继续删除,除非我们提前修改一些数字。


    2.1 关键观察

    f(k)f(k) = 在删除过程中,当需要删除数值 kk,这个数值在数列中出现的次数(包括之前修改过的)。

    如果 f(k)1f(k) \ge 1,这一步就能继续。
    如果 f(k)=0f(k) = 0,我们就必须提前修改某个数,使其变成 kk

    因此,最少修改次数 = 删除过程中遇到的 f(k)=0f(k) = 0kk 的个数。

    f(k)f(k) 是动态的,因为整体加减会影响数值。


    3. 整体偏移的处理

    设整体加的总和为 deltadelta(可以为负)。
    初始 delta=0delta=0,每次 p=0,x=1p=0, x=1delta+=1delta += 1x=1x=-1delta=1delta -= 1

    那么实际数值 aia_i 在比较时,应该用 ai=ai+deltaa_i' = a_i + delta 吗?
    不对,因为删除规则中的 kk 是当前长度,不受整体加减影响。但数列中的数字受整体加减影响。

    更准确地说:
    bi=aideltab_i = a_i - delta 是原始输入调整后的值(这样整体加减时 bib_i 不变)。
    但单点修改时,修改的是 aia_i 而不是 bib_i,所以 bi=aideltab_i = a_i - delta 仍然成立。

    那么比较时:
    当前长度为 kk,我们要看是否存在 bi+delta=kb_i + delta = k 的数,即 bi=kdeltab_i = k - delta

    因此,定义 cnt[v]cnt[v] 表示 bi=vb_i = v 的个数(vv 范围可能很大,因为 deltadelta 会变)。


    4. 转化为 cntcnt 数组问题

    d=deltad = delta(整体加减的偏移量)。

    删除过程:

    • 初始 k=nk = n
    • cnt[kd]1cnt[k-d] \ge 1,则 kkcnt[kd]k \to k - cnt[k-d]
    • 否则,必须修改一次,kk1k \to k - 1(因为我们修改一个数变成 kdk-d

    所以问题变成:
    给定 cntcnt 数组和 dd,模拟删除过程,统计 cnt[kd]=0cnt[k-d] = 0 的次数。


    5. 快速计算答案

    直接模拟每次删除过程是 O(n)O(n) 的,总复杂度 O(nm)O(nm) 不可行。

    我们需要快速计算:

    ans=#{kScnt[kd]=0}\text{ans} = \#\{ k \in S \mid cnt[k-d] = 0 \}

    其中 SS 是删除过程中访问到的 kk 的集合。


    5.1 删除路径的规律

    删除过程是确定的:
    k0=nk_0 = n
    kt+1=ktmax(1,cnt[ktd])k_{t+1} = k_t - \max(1, cnt[k_t - d])
    因为如果 cnt[ktd]1cnt[k_t-d] \ge 1,就减 cnt[ktd]cnt[k_t-d],否则减 1(修改一次)。

    注意 cnt[ktd]=0cnt[k_t-d] = 0 时,kt+1=kt1k_{t+1} = k_t - 1
    cnt[ktd]1cnt[k_t-d] \ge 1 时,kt+1=ktcnt[ktd]k_{t+1} = k_t - cnt[k_t-d]


    5.2 关键优化

    need=kdneed = k - d
    cnt[need]=0cnt[need] = 0 时,kk 会减少 1,然后 needneed 也减少 1(因为 kk 减 1,dd 不变)。
    所以一旦遇到一个 cnt[need]=0cnt[need]=0,就会连续递减直到遇到 cnt[need]1cnt[need] \ge 1

    因此,连续的 needneed 满足 cnt[need]=0cnt[need]=0 的段 会对应一连串的修改。


    5.3 转化为求第一个非零位置

    我们从 k=nk=n 开始,need=ndneed = n-d
    如果 cnt[need]1cnt[need] \ge 1,则 kk 跳到 kcnt[need]k - cnt[need]needneed 变成 kcnt[need]dk - cnt[need] - d
    如果 cnt[need]=0cnt[need] = 0,则这一段连续的 00 都会导致修改,直到 cnt[need]1cnt[need] \ge 1

    因此,问题变成:
    start=ndstart = n-d 开始,每次遇到 cnt[need]=0cnt[need]=0,就沿着 needneed 递减的方向走,直到遇到 cnt[need]1cnt[need] \ge 1,然后跳过一段。


    5.4 数据结构维护

    我们需要快速查询:
    给定 startstart,求最小的 tstartt \le start 使得 cnt[t]1cnt[t] \ge 1,且知道 tt 之后可以跳到 start=tcnt[t]start' = t - cnt[t] 继续。

    这相当于在 cntcnt 数组上做“下一个非零位置”查询,并且支持单点更新(单点修改影响 cnt[bi]cnt[b_i])。

    可以用并查集或链表维护每个位置的下一个非零位置。
    具体地,维护 next[v]next[v] 表示 vv 左边(包括 vv)最近的非零 cntcnt 的位置。
    cnt[v]cnt[v] 从 0 变成 1 时,合并区间;当 cnt[v]cnt[v] 从 1 变成 0 时,拆分区间。


    6. 算法步骤

    1. 初始化 delta=0delta=0bi=aib_i = a_i(因为初始 delta=0delta=0)。
    2. 初始化 cntcnt 数组(用字典或数组,范围 [m,n+m][-m, n+m] 左右)。
    3. 用并查集维护 nextnext 指针:next[v]next[v] 指向 vv 左边最近的 cnt>0cnt>0 的位置,如果没有则为 -\infty
    4. 对于每次修改:
      • 如果是整体加减,更新 deltadelta
      • 如果是单点修改,更新 cntcnt 和并查集。
    5. 计算答案:
      start=ndeltastart = n - delta 开始,
      • 如果 cnt[start]1cnt[start] \ge 1,则 startstartcnt[start]start \to start - cnt[start],继续。
      • 如果 cnt[start]=0cnt[start] = 0,则找到 p=next[start]p = next[start],如果 pp 不存在(pp 太小),则从 startstart11 都要修改,即 startstart 次修改;否则从 startstartp+1p+1 这一段都是 0,修改次数 startpstart - p,然后 startpcnt[p]start \to p - cnt[p]
        重复直到 start0start \le 0,总修改次数就是答案。

    7. 复杂度

    • 每次查询答案的步数 ≤ 修改次数 + 1,因为每次跳跃至少减少 11startstart,并且遇到零段时直接跳到下一个非零位置。
    • 并查集操作近似 O(1)O(1)
    • 总复杂度 O((n+m)α(n+m))O((n+m) \alpha(n+m))

    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. 总结

    这道题的核心是将删空过程转化为对 cntcnt 数组的跳跃查询,利用并查集维护连续零段的快速跳过,从而高效回答多次询问。

    • 1

    信息

    ID
    4924
    时间
    1000ms
    内存
    256MiB
    难度
    10
    标签
    递交数
    1
    已通过
    1
    上传者