題意
給定一個數列 \(\{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';
}
}
這道題啟發我們,做結論題時,可以多考慮答案的下界和上界都取在哪些值上,從而發現關鍵結論或方法。