BJOI2019 删数

落谷Loj

Description

给定一个长度为 \(n\) 的序列 \(a\ (a_i \le n)\)


定义一个删数操作为:

  • 记当前序列长度为 \(k\) ,则删除数列中所有等于 \(k\) 的数。

如果能在有限次进行下列删数操作后将其删为空数列,则称这个数列可以删空。


给定 \(m\) 次修改操作:

  • 单点修改
  • 数列整体 \(+1/-1\)

求每次操作后的序列 \(a\) 来说,至少还需要修改几个数,才能将这个数删空?

Solution

子问题 1

我们先来看一个子问题,什么样的长度为 \(len\) 序列可以一下子删空?

首先答案和序列顺序无关,可以考虑把数打到值域上去,设数 \(i\) 出现的次数是 \(cnt_i\)

不妨逆向思维一下,现在给你一个可以一下删空的元素,然后你还可以加入几个元素,让序列还是可以删空,这样样的数怎么加?

举个例子,比如当前序列是 \(\text{1 3 3}\),很容易发现你可以加 \(1\)\(4\),或者 \(2\)\(5\),或者 \(3\)\(6\) 等等...这样可以一开始删的时候把我们新增的玩意全部删除。总结一下规律,设序列长度是 \(n\),你可以加入 \(k\)\(n + k\)

这样再反过来想,完全删数的这个操作是无缝连接的,设序列当前长度为 \(len\),重复执行:

  1. \(len \gets len - cnt[len]\)

如果最终 \(len\) 可以变成 \(0\) 就是成功了 !

所以可以看作一个数轴,一个人初始在 \(len\) 点,然后 \(cnt[len]\) 表示从这个点出发,可以跳到 \(len - cnt[len]\),可以删完 \(\Leftrightarrow\) 可以跳到 \(0\)

这样的话,可以将 \(cnt[i]\) 看作一条线段 \([i - cnt[i] + 1, i]\),(即从 \(len\) 出发的路线)。能够删完当且仅当路线无缝连接。由于 \(\sum cnt = len\),而我们的目标从 \(len\) 走到 \(0\) 步长也是 \(len\),所以不会出现线段重叠也能删完的情况。

子问题 2

对于一个序列而言,至少还需要修改几个数,才能将这个数删空?

先给出结论:将 \(1 \le i \le n\) 的每条线段 \([i - cnt[i] + 1, i]\) 打到数轴上(也可以理解为这个区间 \(+1\)),操作的最小次数就是值域 \([1, n]\) 中,没有线段经过的点数(也可以理解为区间 \([1, n]\)\(0\) 的个数)。

必要性

考虑每次修改一个数最多只能使一条线段伸长一个长度(当然还会有一条线段缩短),所以最多只会减少一个 \(0\),所以 \(ans \le\) \(0\) 的个数。

充分性

我们考虑构造一组解。

  • 每次选出一个其线段左端点覆盖 \(> 1\) 次的数,或者不在 \([1, n]\) 值域内的数,将其换成一个当前无线段覆盖的数,这样可以将一个 \(0\) 去掉。

然后就感性的证明了最小操作次数 \(=\) \(0\) 的个数。

如何支持修改?

如果朴素的用我们的结论是 \(O(n^2m)\) 的(每次修改都 \(O(n^2)\) 全部搞一遍)。

然后观察一下我们需要支持的操作:

  • 区间即 \([1, n]\)\(0\) 的个数
  • 区间修改(因为整体平移导致一些线段不能用,所以要动态删除/加入)
  • 整体平移(即所有线段往右边/往左边平移一个单位)
  • 单点修改(即线段延长缩短带来的效应)

因为整体平移,所以不会

如果没有整体平移,是个线段树。

既然我们不会整体平移的数据结构,但是我们知道区间求 \(0\) 的个数的这个区间是唯一的,既然线段不能平移,那么我们就平移查询的区间骂。维护一个 \([L, R]\) 初始 \(L = 1, R = n\)\([L, R]\) 表示当前 \([1, n]\) 这个值域的区间在线段树下体现的编号是什么。

  • 对于单点修改,发现只有两个线段变化了(分别是左端点缩短和伸长),直接修改即可,注意这个数在不在当前的值域 \([1, n]\) 里,如果不在不能修改。

  • 对于整体平移。令 \(L \gets L - x, R \gets R -x\)(可以理解为编号不动,如果线段往右边移动,那么查询区间就往左边移动,相反的情况的对称的)。然后注意加入新的集合线段/删除旧的线段(右端点不在 \([1, n]\) 的线段)。

  • 对于区间求 \(0\) 的个数。佛了不会 因为这题权值是非负的,所以记录最小值和最小值出现的次数就行。。。

时间复杂度

\(O((N + M) \log_2 N)\)

Tips

  • 注意到 \(L, R\) 可能被移动到 \(+/-m\) 的情况,并且左端点最多伸出 \(n\) 的长度,所以线段树要开 \(4\) 倍,左右区间要开到 \([1, 2(n + m)]\)
  • \(cnt_i\) 在平移意义下并不是 \(i\) 这个数字了,而是一个编号
  • \([L, R]\) 维护的是值域 \([1, n]\) 的位置,所以对于新进来的一个数 \(x\),他应该在的编号是 \(x + L - 1\)

Code

#include <iostream>
#include <cstdio>

using namespace std;

const int N = 150005;

int n, m, tot, a[N], cnt[N * 4];

int tag[N * 16];

struct Node{
	int v, cnt;
	Node (){}
	Node (int v, int cnt): v(v), cnt(cnt) {}
	Node (Node a, Node b) {
		v = min(a.v, b.v);
		cnt = (v == a.v ? a.cnt : 0) + (v == b.v ? b.cnt : 0);
	}
} dat[N * 16];

void inline pushup(int p) {
	dat[p] = Node(dat[p << 1], dat[p << 1 | 1]);
}

void inline pushdown(int p) {
	if (tag[p]) {
		dat[p << 1].v += tag[p], dat[p << 1 | 1].v += tag[p];
		tag[p << 1] += tag[p], tag[p << 1 | 1] += tag[p];
		tag[p] = 0;
	}
}

void build(int p, int l, int r) {
	if (l == r) { dat[p] = Node(0, 1); return; }
	int mid = (l + r) >> 1;
	build(p << 1, l, mid);
	build(p << 1 | 1, mid + 1, r);
	pushup(p);
}

void change(int p, int l, int r, int x, int y, int k) {
	if (x > y) return;
	if (x <= l && r <= y) {
		dat[p].v += k, tag[p] += k;
		return;
	}
	pushdown(p);
	int mid = (l + r) >> 1;
	if (x <= mid) change(p << 1, l, mid, x, y, k);
	if (mid < y) change(p << 1 | 1, mid + 1, r, x, y, k);
	pushup(p);
}

int query(int p, int l, int r, int x, int y) {
	if (x <= l && r <= y) return dat[p].v == 0 ? dat[p].cnt : 0;
	pushdown(p);
	int mid = (l + r) >> 1, res = 0;
	if (x <= mid) res += query(p << 1, l, mid, x, y);
	if (mid < y) res += query(p << 1 | 1, mid + 1, r, x, y);
	return res;
}

void inline add(int x, int k) { 
    change(1, 1, tot, x - cnt[x] + 1, x, k);
}

int main() {
	scanf("%d%d", &n, &m);
    tot = (n + m) * 2;
	int l = n + m + 1, r = n + m + n;
	for (int i = 1; i <= n; i++) scanf("%d", a + i), a[i] += l - 1, cnt[a[i]]++;
	build(1, 1, tot);
	for (int i = l; i <= r; i++) change(1, 1, tot, i - cnt[i] + 1, i, 1);
	while (m--) {
		int p, x; scanf("%d%d", &p, &x);
		if (p) {
			x += l - 1;
			if (l <= a[p] && a[p] <= r) {
				change(1, 1, tot, a[p] - cnt[a[p]] + 1, a[p] - cnt[a[p]] + 1, -1);
			}
			cnt[a[p]]--, a[p] = x, cnt[a[p]]++;
			if (l <= a[p] && a[p] <= r) {
				change(1, 1, tot, a[p] - cnt[a[p]] + 1, a[p] - cnt[a[p]] + 1, 1);
			}
		} else {
			l -= x, r -= x;
			if (x == 1) add(r + 1, -1), add(l, 1);
			else add(l - 1, -1), add(r, 1);
		}
		printf("%d\n", query(1, 1, tot, l, r));
	}
	return 0;
}
posted @ 2020-04-05 20:22  DMoRanSky  阅读(279)  评论(0编辑  收藏  举报