P5324 題解

CTHOOH發表於2024-07-02

題意

給定一個數列 \(\{a_n\}\),定義一次刪除操作為:假設當前序列長度為 \(len\),刪除序列中所有等於 \(len\) 的數。

現在有 \(m\) 個操作,每次操作為單點修改或整體加減。每次操作完後,你需要修改若干個數,使得序列能夠在若干次刪除操作後被刪空,求最小修改次數。

資料範圍:\(1 \le n, m \le 1.5 \cdot 10^5, 0 \le p \le n, 1 \le x, a_i \le n\)

思路

這種操作題肯定是先找出一種靜態求取答案的方法,然後再用資料結構等東西維護這個答案。

那麼如何求取答案呢?首先按照題意,序列裡面不同元素的順序是無關緊要的,我們只需要看每個數的出現次數即可。而知道了這一性質後該怎麼辦呢,蒟蒻開始罰坐。。。太菜了 QAQ 其實這是一道結論題,有結論:

假設元素 \(i\) 的出現次數為 \(cnt_i\),將區間 \([i - cnt_i + 1, i]\) 覆蓋,答案為 \([1, n]\) 中未被覆蓋的點的數量。

考慮證明:

  • 這一定是答案的上界,有一種構造方法是:把一些被覆蓋多次的點搬到未被覆蓋的位置上,所以答案一定 $\le $ 這種構造方式。
  • 這一定是答案的下界,因為一個點未被覆蓋,說明比它大的點到不了它,這樣就無法到達比它小的點,故不應存在未被覆蓋的點。

於是就可以維護了,對於單點修改操作,很顯然。對於整體加減操作,相當於平移 \([1, n]\) 的區間,當 \(x\) 被移出區間時,對 \([x - cnt_x + 1, x]\) 進行區間減一,加進來時區間加一即可。可以用線段樹維護最小值和最小值出現次數,時間複雜度 \(O(n \log n)\)。程式碼稍微有點細節:

#include <bits/stdc++.h>
#define rep(i, l, r) for (int i = l; i <= r; ++i)
#define per(i, r, l) for (int i = r; i >= l; --i)
#define re(i, n) for (int i = 0; i < n; ++i)
#define pe(i, n) for (int i = n - 1; i >= 0; --i)
using namespace std;
using i64 = long long;
const int N = 9E5 + 5;
int n, m, a[N], L = 3E5;
int cnt[N];
struct segt {
    int tag[N << 2];
    struct node {
        int mn = 1E9;
        int cnt = 0;
    } t[N << 2];
    friend node operator+(node a, node b) {
        node c;
        c.mn = min(a.mn, b.mn);
        if (a.mn == c.mn) c.cnt += a.cnt;
        if (b.mn == c.mn) c.cnt += b.cnt;
        return c;
    }
    void pull(int x) {t[x] = t[x * 2] + t[x * 2 + 1];}
    void build(int x, int l, int r) {
        tag[x] = 0;
        if (l == r) {t[x].cnt = 1; t[x].mn = 0; return ;}
        int mid = (l + r) / 2;
        build(x * 2, l, mid);
        build(x * 2 + 1, mid + 1, r);
        pull(x);
    }
    void apply(int x, int val) {tag[x] += val; t[x].mn += val;}
    void push(int x) {
        apply(x * 2, tag[x]);
        apply(x * 2 + 1, tag[x]);
        tag[x] = 0;
    }
    void modify(int x, int l, int r, int L, int R, int val) {
        if (r < L || l > R) return ;
        if (l >= L && r <= R) return apply(x, val);
        int mid = (l + r) / 2; push(x);
        modify(x * 2, l, mid, L, R, val);
        modify(x * 2 + 1, mid + 1, r, L, R, val);
        pull(x);
    }
    void modify(int l, int r, int val) {
        modify(1, 1, 3 * L, l, r, val);
    }
    void modify(int pos, int val) {modify(pos, pos, val);}
    node query(int x, int l, int r, int L, int R) {
        if (r < L || l > R) return {};
        if (l >= L && r <= R) return t[x];
        int mid = (l + r) / 2; push(x);
        return query(x * 2, l, mid, L, R) + query(x * 2 + 1, mid + 1, r, L, R);
    }
    int query(int l, int r) {
        auto temp = query(1, 1, 3 * L, l, r);
        if (temp.mn != 0) return 0;
        return temp.cnt;
    }
} t;
int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);
    cin >> n >> m;
    t.build(1, 1, 3 * L);
    rep(i, 1, n) cin >> a[i], ++cnt[a[i] + L];
    rep(i, L + 1, L + n) if (cnt[i]) t.modify(i - cnt[i] + 1, i, 1);
    int tag = 0; while (m--) {
        int p, x; cin >> p >> x;
        if (p) {
            if (a[p] >= 1 - tag && a[p] <= n - tag)
                t.modify(a[p] + L - cnt[a[p] + L] + 1, -1);
            --cnt[a[p] + L];
            a[p] = x - tag;
            if (a[p] >= 1 - tag && a[p] <= n - tag)
                t.modify(a[p] + L - cnt[a[p] + L], 1);
            ++cnt[a[p] + L];
        } else {
            #define modi(pos, val) t.modify(L + pos - cnt[pos + L] + 1, L + pos, val) 
            if (x > 0 && cnt[n - tag + L]) modi((n - tag), -1);
            if (x < 0 && cnt[1 - tag + L]) modi((1 - tag), -1);
            tag += x;
            if (x > 0 && cnt[1 - tag + L]) modi((1 - tag), 1);
            if (x < 0 && cnt[n - tag + L]) modi((n - tag), 1);
        }
        cout << t.query(L + 1 - tag, L + n - tag) << '\n';
    }
}

這道題啟發我們,做結論題時,可以多考慮答案的下界和上界都取在哪些值上,從而發現關鍵結論或方法。