zoukankan      html  css  js  c++  java
  • BJOI2019 删数

    落谷Loj

    Description

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


    定义一个删数操作为:

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

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


    给定 (m) 次修改操作:

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

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

    Solution

    子问题 1

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

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

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

    举个例子,比如当前序列是 ( ext{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
    ", query(1, 1, tot, l, r));
    	}
    	return 0;
    }
    
  • 相关阅读:
    人生如此
    微软十七道智力面试题及答案
    【Flink系列十】Flink作业提交过程的调试和诊断
    【Flink系列九】Flink 作业提交遇到的问题记录以及原理
    Jackson ObjectMapper JSON序列化工具使用笔记,由浅入深
    既有设计模式的lambda重构
    观察者模式/Observer
    函数式接口java.util.function
    面向对象世界的七个设计原则
    重构-改善既有代码设计读后灵光
  • 原文地址:https://www.cnblogs.com/dmoransky/p/12638844.html
Copyright © 2011-2022 走看看