【题解】有便便的厕所(权值线段树动态开点模板题)

我只是来填坑的。

关于权值线段树的更多有关内容见此。

题面

题目描述

众所周知,\(\texttt{GM}\) 家的狗特别喜欢拉便便。\(\texttt{GM}\) 为了方便它方便,在家里修建了 \(10^9\) 个马桶,依次排开,成一条直线,为了方便,依次编号 \(1\)\(10^9\)

\(\texttt{GM}\) 家的狗叫“地铺雀儿”,“地铺雀儿”每次会选择一个马桶方便,但是很不幸,它不会冲厕所。\(\texttt{GM}\) 为了冲厕所方便,修建了一个巨型水桶,可以一次冲掉一个区间内的每个厕所。(当然,区间上如果有没用过的厕所,也会一起冲,这样也许有些浪费水)

“地铺雀儿”还有一个特殊的爱好,它想观察某个区间上所有便便排序后的第 \(k\) 大便便在哪个厕所中,以方便跟它朋友吹嘘自己多能能拉便便。比如它观察区间 \(2\sim 10\) 中,想找出有便便的编号第 \(2\) 大的厕所,有 \(2,4,5,6\) 四个厕所各有一坨便便,那么答案就是5。

注意:“地铺雀儿”不怎么讲卫生,如果某厕所有便便了,它还可以继续在这个厕所拉便便。(也就是这个厕所里有多个便便),这些便便也要参加排序。

给你 \(q\) 个操作:

操作 \(1\) 的格式是:1 x,表示“地铺雀儿”在x号位置拉便便;

操作 \(2\) 的格式是:2 l r,表示 \(\texttt{GM}\) 冲掉区间 \([l,r]\) 之间的所有便便,如果一个厕所有多个便便也一并冲走。

操作 \(3\) 的格式是:3 l r k,表示“地铺雀儿”想在区间l到r的范围内,包括 \(l\)\(r\) ,寻找第 \(k\) 大的厕所编号(若不存在输出-1)

输入格式

第一行输入一个 \(q\),代表询问次数 \((q\leqslant10^5)\) 接下来 \(q\) 行,每行先输入一个 \(op\),如果op==1,则只输入一个 \(x\) ;若op==2,则输入 \(l,r\) ;若op==3,则需要输入 \(l,r,k\)

输出格式

输出op==3时的所有询问

样例

样例输入

20
1 3
1 2
1 1
1 1
1 2
1 3
1 2
1 3
3 1 3 1
3 1 3 2
3 1 3 3
3 1 3 4
3 1 3 5
3 1 3 6
3 1 3 7
3 1 3 8
3 1 2 1
3 1 2 2
3 1 2 3
3 1 2 4

样例输出

3
3
3
2
2
2
1
1
2
2
2
1

数据范围与提示

1234.jpg

Solution

总结一下题意:

有一些带权值的 shit\(q\) 个操作 \((1\leqslant q\leqslant 10^5)\) ,操作分为三种:

  • 操作一:输入 \(x\) ,将权值为 \(x\)shit 的数量增加 \(1\) (权值线段树动态开点中的Insert
  • 操作二:输入 \(l,r\) ,将权值在 \([l,r]\) 范围内的 shit 的数量清空 (区间修改中的Update
  • 操作三:输入 \(l,r,k\) ,输出权值在 \([l,r]\) 范围内,权值第 \(k\) 大的 shit 。(权值线段树中的Query

那么,我们发现,操作二是个区间修改,这样一来所有函数都需要Spread,所以我们先来解决操作二。

区间修改还是很好写的,首先我们先规定每一个结点存放的值:

int l,r,ll,rr;
//左儿子编号,右儿子编号,左端点,右端点
bool add;
//懒标记,标记当前值域是否被清空,因为清空只有一种固定的方式,所以将add定义为bool变量
int num;
//记录当前值域每个数的数量之和
//(不要问我为什么不写sum,因为我的sum写着写着就变成了num了)

那么Spread 就非常好打了:

void Spread(int p){
	if(a[p].add){
		int lt=a[p].l;//存左子树的编号
		int rt=a[p].r;//存右子树的编号
		//-----延伸-----
		a[lt].num=0;//清空
		a[lt].add=1;//懒标
		a[rt].num=0;//清空
		a[rt].add=1;//懒标
		//----清除-----
		a[p].add=0;//去懒标
	}
	return;
}

Spread都出来了,还愁什么Update

首先,打出一份动态开点区间修改的板子。

因为是动态开点,我们还是要处理一下当前值域是否存在的问题。

如果当前值域不存在,也就是递归到的结点编号为默认的 \(0\) 时,就可以return了。

因为当前结点不存在,说明之前根本没有被修改过,值域中每个数出现的次数都是 \(0\) 次,相当于已经是空的了,也就不用再多此一举,手动清空了。

void Update(int p,int l,int r){
	if(!p)return;
	if(l<=a[p].ll&&a[p].rr<=r){
		a[p].num=0;
		a[p].add=1;
		return;
	}
	Spread(p);
	int lt=a[p].l,rt=a[p].r;
	int mid=a[p].ll+a[p].rr>>1;
	if(l<=mid)Update(lt,l,r);
	if(r>mid)Update(rt,l,r);
	a[p].num=a[lt].num+a[rt].num;
	return;
}

氮素,区间修改的次数太多也是有可能T的,我们思考,怎么减少区间修改的次数?

\(p\) 所表示的值域已经被打过懒标记了,就说明这个值域之前已经被清空过了,并且其中的数的数量没有新增(否则Insert会调用Spread\(p\) 的懒标去掉)

如果这个值域中数出现的次数之和为 \(0\) ,那么也不用清空了,本身就是空的嘛。

void Update(int p,int l,int r){//区间修改
	if(!p||a[p].add||!a[p].num)
		return;
	if(l<=a[p].ll&&a[p].rr<=r){
		a[p].num=0;
		a[p].add=1;
		return;
	}
	Spread(p);
	int lt=a[p].l,rt=a[p].r;
	int mid=a[p].ll+a[p].rr>>1;
	if(l<=mid)Update(lt,l,r);
	if(r>mid)Update(rt,l,r);
	a[p].num=a[lt].num+a[rt].num;
	return;
}

好的,那么接下来就是Insert

往正常的Insert里塞个Spread就行惹。

void Insert(int p,int l,int r,int x){//单点修改 
	a[p].ll=l,a[p].rr=r;
	if(l==r){
		a[p].add=0;
		a[p].num++;
		return;
	}
	int mid=l+r>>1;
	Spread(p);
	if(x<=mid){
		if(!a[p].l)a[p].l=++tot;
		Insert(a[p].l,l,mid,x);
	}
	else{
		if(!a[p].r)a[p].r=++tot;
		Insert(a[p].r,mid+1,r,x);
	}
	a[p].num++;
	return;
}

最后,是最难搞的Query

编号第 \(k\) 大,要怎么处理呢?

我们的权值线段树从左到右表示的数/值域是从小到大的,那么,更大的就在偏向右边的地方。(笔者表达能力不好,见谅)

如果我们要查询的区间 \([l,r]\) 被右子树包含一部分,我们就看:

权值线段树示意图
若①值域中包含的数的个数 \(\geqslant k\) ,说明第 \(k\) 大数就在①值域中,递归询问右子树。

相应的,如果①值域中包含的数的个数不到 \(k\) ,说明第 \(k\) 大的数在左子树中,递归询问左子树。

注意,此时虽然右子树没有 \(k\) 那么多个数,但是也有自己的包含数的个数(假设为 \(rdat\) 个),查询左子树时就不能再查第 \(k\) 大的了,而是 \(k-rdat\) 大的数。

int QuerySum(int p,int l,int r){
	if(!p||a[p].add||!a[p].num)
		return 0;//理由同 Update,这些情况都是包含数的个数为0的
	if(l<=a[p].ll&&r>=a[p].rr)
		return a[p].num;
	int val=0;
	Spread(p);
	int lt=a[p].l,rt=a[p].r;
	int mid=a[p].ll+a[p].rr>>1;
	if(l<=mid)val+=QuerySum(lt,l,r);
	if(r>mid)val+=QuerySum(rt,l,r);
	return val;
} 
int Query(int p,int l,int r,int k){
	if(!p||a[p].add||!a[p].num)
		return -1;//理由同 Update,如果一个数也没有,也就没有第k大数的说法
	if(a[p].ll==a[p].rr){
		if(a[p].num>=k)
			return a[p].ll;
		return -1;
	}
	Spread(p);
	int lt=a[p].l,rt=a[p].r;
	int rdat=0;//保存右子树的查询数据
	int mid=a[p].ll+a[p].rr>>1;
	if(r>mid){
		rdat=QuerySum(rt,l,r);
		if(k<=rdat)
		return Query(rt,l,r,k);
	}
	if(l<=mid)
		return Query(lt,l,r,k-rdat);//即使右子树没有包含询问区间,rdat也是0,不会对k产生影响
	return -1;
}

Code

坑坑洼洼的代码

#include<cmath>
#include<cstdio>
const int maxn=1e9;
const int maxm=1e5*log(1e9)/log(2);
#define int long long
struct Segment_tree{
	int l,r;
	bool add;
	int num,ll,rr;
}a[maxm];
int n,tot,type,l,r,x;
void Spread(int p){
	if(a[p].add){
		int lt=a[p].l;
		int rt=a[p].r;
		a[lt].num=0;
		a[lt].add=1;
		a[rt].num=0;
		a[rt].add=1;
		a[p].add=0;
	}
	return;
}
void Insert(int p,int l,int r,int x){//单点修改 
	a[p].ll=l,a[p].rr=r;
	if(l==r){
		a[p].add=0;
		a[p].num++;
		return;
	}
	int mid=l+r>>1;
	Spread(p);
	if(x<=mid){
		if(!a[p].l)a[p].l=++tot;
		Insert(a[p].l,l,mid,x);
	}
	else{
		if(!a[p].r)a[p].r=++tot;
		Insert(a[p].r,mid+1,r,x);
	}
	a[p].num++;
	return;
}
void Update(int p,int l,int r){//区间修改
	if(!p||a[p].add||!a[p].num)
		return;
	if(l<=a[p].ll&&a[p].rr<=r){
		a[p].num=0;
		a[p].add=1;
		return;
	}
	Spread(p);
	int lt=a[p].l,rt=a[p].r;
	int mid=a[p].ll+a[p].rr>>1;
	if(l<=mid)Update(lt,l,r);
	if(r>mid)Update(rt,l,r);
	a[p].num=a[lt].num+a[rt].num;
	return;
}
int QuerySum(int p,int l,int r){
	if(!p||a[p].add||!a[p].num)
		return 0;
//	printf("QuerySum->[%d,%d]|%d",a[p].ll,a[p].rr,a[p].num);
	if(l<=a[p].ll&&r>=a[p].rr)
		return a[p].num;
	int val=0;
	Spread(p);
	int lt=a[p].l,rt=a[p].r;
	int mid=a[p].ll+a[p].rr>>1;
	if(l<=mid)val+=QuerySum(lt,l,r);
	if(r>mid)val+=QuerySum(rt,l,r);
	return val;
} 
int Query(int p,int l,int r,int k){
	if(!p||a[p].add||!a[p].num)
		return -1;
	if(a[p].ll==a[p].rr){
		if(a[p].num>=k)
			return a[p].ll;
		return -1;
	}
	Spread(p);
	int lt=a[p].l,rt=a[p].r;
	int rdat=0;
	int mid=a[p].ll+a[p].rr>>1;
//	printf("Query->[%d,%d]\n",a[p].ll,a[p].rr);
	if(r>mid){
//		printf("[%d,%d]:right\n",a[p].ll,a[p].rr);
		rdat=QuerySum(rt,l,r);
//		printf("=%d\n",rdat);
		if(k<=rdat)
		return Query(rt,l,r,k);
	}
	if(l<=mid){
//		printf("[%d,%d]:left\n",a[p].ll,a[p].rr);
		return Query(lt,l,r,k-rdat);
	}
	return -1;
}
signed main(){
	scanf("%lld",&n);++tot;
	for(int i=1;i<=n;++i){
		scanf("%lld",&type);
		if(type==1){
			scanf("%lld",&x);
			Insert(1,1,maxn,x);
		}
		else if(type==2){
			scanf("%lld%lld",&l,&r);
			Update(1,l,r);
		}
		else{
			scanf("%lld%lld%lld",&l,&r,&x);
			printf("%lld\n",Query(1,l,r,x));
		}
	}
	return 0;
}

end.

posted @ 2020-12-19 23:01  XSC062  阅读(157)  评论(1编辑  收藏  举报