洛谷5324 删数

这种题看起来很难确定如何给出一个简单的判别方法去判断是否符合条件的一般都是证明下界再构造下界

首先给出结论:对于一个数列,某一个数字\(i\)的个数有\(cnt[i]\)个,那么此数字可以覆盖一个区间\([i-cnt[i]+1,i]\),遍历\(1\) ~ \(n\)每一个数字并更新每个数字被当前区间覆盖的次数,最后答案就是\([1,n]\)中没有被覆盖到的数字的个数

证明:先证一个数列能被删除,当且仅当\([1,n]\)中每一个数字都被覆盖了一遍,而且只被覆盖了一遍

该结论的充分性显然

必要性:由于\(cnt\)的和为\(n\),所以此时数列中最大的数一定是\(n\)(因为\(n\)这一个位置要被覆盖,只能有\(n\)或者比\(n\)更大的数去覆盖,然而如果存在比\(n\)更大的数,\([1,n]\)这个区间肯定不能被覆盖完,所以一定有\(n\)而且最大),那么把若干个\(n\)删除后重复利用此结论可以得证

如果某一个数字产生的区间的左边界小于\(1\)(也就是这个数字为非正数)或者某个数字产生的区间的右边界大于\(n\)(也就是这个数字大于\(n\)),那么这个数字一定会被全部修改(不然不可能能够被删空),而修改数的顺序是无关紧要的,所以我们可以先把所有这种数字的个数记下来(设为\(x\)),然后忽略这些数(不去计算这些数字产生的区间覆盖),那么我们的操作就变成了如下两种:一,让某一个\([1,n]\)中的数字加一,花费为\(1\),至多操作\(x\)次;二,让某一个\([1,n]\)中的数字减一并让另一个\([1,n]\)中的数字加一,花费为\(1\),可以操作无限次

无论是操作一还是操作二,一次操作最多只能让\([1,n]\)中一个没有被覆盖的数被覆盖,所以下界就是此时\([1,n]\)中没有被覆盖的数字的个数

那么我们接下来构造一个方法来达到这个下界,为了方便说明,我们约定\(a[i]\)表示\(i\)这个数字被覆盖的次数(\(a[i]\)\(i\)是可以等于负数的)

由于\(cnt\)的和为\(n\)(即\(\sum a[i]=n\))且原数列长为\(n\),此时没被覆盖的数字的个数(即\(a[i]=0\)\(i\)的个数)\(=x+\sum_{a[i]>1且i>0}(a[i]-1)+\sum_{i≤0}a[i]\),我们每次操作让被重复覆盖的数字被覆盖的次数减一(注意每次一定可以找到产生这个覆盖区间的数,因为只要存在被重复覆盖的数字,我们观察这些数字中最小的一个(设为\(k\)),他前面的数字都没有被重复覆盖,所以让这一个数字被重复覆盖的数,假设有若干个,那么一定最多只有一个数产生的区间是会比\(k\)还要小的,我们修改这些数中的其他数就好了)或者让产生区间左边界小于\(1\)的数字的个数减少一个或者用最开始存储的\(x\)减一,然后放到某一个空位上,每一次都这么干。易知三种操作不会互相影响(也就是一次操作完了之后不会增加没有被覆盖的数的个数),最后刚好达到下界

证毕

那么考虑修改,对整个数列加一或者减一,就是让所有产生的区间往旁边移动一位,我们采用相对性的思路,让查询区间往旁边移动一位,这样就可以极大降低复杂度

这里线段树维护要用到类似扫描线的方法

update 2024.5.16

独立做出来了,来说一下怎么想到的

首先就像上面说的一样,很容易发现如果能删除的话,最大的数字一定是\(n\);然后我们分类讨论\(n\)的个数如果只有一个,那么显然剩下的最大的数就是\(n-1\),如果只有两个,那么剩下的最大的数就是\(n-2\),依次类推,如果有\(i\),那么剩下的最大的数就是\(n-i\),然后我们重复利用此过程,就会发现一个数列能被删除,当且仅当\([1,n]\)中每一个数字都被覆盖了一遍,而且只被覆盖了一遍

然后是代码的问题,非常复杂,光代码就写了两个半小时

首先我没有用扫描线维护

我们按照上述过程分析,就可以知道,假设我们将序列分成三段,分别是下标小于等于\(0\)的,下标介于\(1\)\(n\)之间的,下标大于\(n\)的(假设还没有偏移),那么答案就是第一段和第三段的和,加上第二段的和减去(\(n\)减去第二段\(0\)的数量),化简就是没有被覆盖到的数字的个数(也就是第二段\(0\)的数量)

所以我们只用考虑维护序列\(0\)的数量就好了

然后跟"Atlantis"很像了,这里就要用扫描线(去想一下,还没认真想),但其实有另一种方法

定义结构体

struct node
{
	int l,r;
	int lazy,summin,Min;//summin表示这个区间最小值的数量
}t[(N*3)<<2];

于是我们实时维护最小值的数量就好了,修改函数也比较简单

void modify(int p,int x,int y,int d)
{
	if(t[p].l>y||t[p].r<x) return;
	if(t[p].l>=x&&t[p].r<=y)
	{
		t[p].lazy+=d;
		t[p].Min+=d;
		return;
	}
	putdown(p);
	modify(p<<1,x,y,d),modify(p<<1|1,x,y,d);
	if(t[p<<1].Min<t[p<<1|1].Min) t[p].summin=t[p<<1].summin;
	else if(t[p<<1].Min>t[p<<1|1].Min) t[p].summin=t[p<<1|1].summin;
	else t[p].summin=t[p<<1].summin+t[p<<1|1].summin;//这三个讨论非常easy
	t[p].Min=min(t[p<<1].Min,t[p<<1|1].Min);
}

那么最终在查询的时候,先查询最小值,如果说最小值不是\(0\),那么答案就是\(0\),否则直接输出\(0\)的个数就好了

但是其实这道题目最难的是偏移的问题,有点绕

我们偏移的思路是不动数字动区间,而真实情况应该是不动区间动数字

比如,当前区间为\([-2,8]\),数字分别为1 2 3 4 5,那么真实的区间就是\([1,11]\),真实的数字分别为4 5 6 7 8

我们用一个数组cnt去记录每一个数字的数量,但是偏移的时候到底应该算什么呢?

先来看整体加一减一的情况

     ...
        if(!op) 
		{
			if(x==1)
			{
				if(cnt[R]) modify(1,R-cnt[R]+1,R,-1);
				modify(1,R,R,cnt[R]);
			}
			else 
			{
				modify(1,R+1,R+1,-cnt[R+1]);
				if(cnt[R+1]) modify(1,R+1-cnt[R+1]+1,R+1,1);
			}
			L-=x,R-=x,delta+=x;
		}
    ...

我们肯定是没办法偏移cnt数组的,所以任意时候,cnt[i]就表示当前区间下标为\(i\)的数字的个数(假设偏移量为delta,那么也就是真实区间中数字\(i+delta\)的个数);由于我们只移动区间,所以我们直接获得当前区间的下标就好了,不要用诸如cnt[R-delta]这种东西

再来看看单点修改

    ...
        else  
		{
			int y=a[op];
			x-=delta;
			if(x==y) goto L;
			if(y+m<=R) modify(1,y+m-cnt[y+m]+1,y+m-cnt[y+m]+1,-1);
			else modify(1,y+m,y+m,-1);
			if(x+m<=R) modify(1,x+m-cnt[x+m],x+m-cnt[x+m],1);
			else modify(1,x+m,x+m,1);
			cnt[x+m]++,cnt[y+m]--;
			a[op]=x;
		}
    ...

任意时刻,a[i]记录的是真实区间中下标为\(op\)的真实数字减去\(delta\)的值,也就是说\(a[i]+delta\)就是真实区间中\(op\)位置的真实值,反映在当前区间中的值就是\(a[i]+delta-delta=a[i]\);由于\(x\)是真实值,所以要给\(x\)减去\(delta\);然后同理注意\(cnt\)的括号里面不要打偏移量,我们都是在当前区间进行操作的

posted @ 2023-10-05 17:09  最爱丁珰  阅读(4)  评论(0编辑  收藏  举报