ZJOI 2013 K大数查询 题解

题目传送门

题目大意: 现在你有n个集合,每次给集合 l l l~ r r r 一人一个 c c c,或者是询问集合 l l l ~ r r r 中的第k大。

题解

然而这个博客是个不小的坑。

本人脑子一热,打了个几乎没人打的 s p l a y splay splay 套线段树。

具体做法:线段树作为外层树,维护集合区间,在每个点里面开一棵 s p l a y splay splay,维护这个区间内的集合里面的数。

区间加点这个操作可以打 l a z y lazy lazy标记,但是标记不下传,如果下传了的话一个点会变成两个点(本来就我带一个,传下去之后儿子一人一个),会拉低时间复杂度。所以考虑用标记永久化,也就是标记不下传,询问的时候把一路上遇到的标记都统计起来即可,效果是一样的,但是会快很多。

我的线段树上的每个点要开两棵 s p l a y splay splay,一棵维护管理区间内的数,一棵维护标记。

然而这样树套树常数巨大,在调了半天之后解决了WA的问题,然而TLE却是硬伤。

个人觉得一个大问题就是询问的时间复杂度是 O ( l o g 3 n ) O(log^3n) O(log3n)的……因为外面要二分,毕竟这样子树套树无法直接求出排名为k的数,只能二分每个数求他们的排名。

所以这个代码仅供参考学习qwq(并不能AC,但是实测正确性是有的,只是比较慢):

#include <cstdio>
#include <cstring>
#define ll long long

int n,m;
struct node{//splay
	int x,tot,size;//x:值,tot:有多少个相同的点,size:子树的大小
	node *zuo,*you,*fa;//左右儿子和父亲
	node(int c,int Size,node *father):x(c),tot(Size),size(Size),zuo(NULL),you(NULL),fa(father){};
	node *find(int c)//找到最接近c的节点
	{
		if(zuo!=NULL&&c>x)return zuo->find(c);
		else if(you!=NULL&&c<x)return you->find(c);
		else return this;
	}
	void check()//维护size
	{
		size=tot;
		if(zuo!=NULL)size+=zuo->size;
		if(you!=NULL)size+=you->size;
	}
};
struct two{//v表示这个数在这个区间中是否有出现
//假如有出现(false),xy表示这个数的排名范围
//假如没出现(true),xy表示这个数比多少个数大(此时xy相等)
//可以在下面结合代码理解
	ll x,y;bool v;
	two operator +(const two p){return (two){x+p.x-(!v)-(!p.v)+(!(v&p.v)),y+p.y,v&p.v};}
};
void rotate(node *x)//splay板子,没啥好讲的
{
	node *fa=x->fa,*gfa=fa->fa;
	if(fa->zuo==x)
	{
		fa->zuo=x->you;
		if(x->you!=NULL)x->you->fa=fa;
		x->you=fa;
	}
	else
	{
		fa->you=x->zuo;
		if(x->zuo!=NULL)x->zuo->fa=fa;
		x->zuo=fa;
	}
	x->fa=gfa;fa->fa=x;
	if(gfa!=NULL)
	{
		if(gfa->zuo==fa)gfa->zuo=x;
		else gfa->you=x;
	}
	fa->check();x->check();
}
#define which(x) (x->fa->zuo==x)
void splay(node *x,node *to,node *&rt)
{
	while(x->fa!=to)
	if(x->fa->fa!=to&&which(x)==which(x->fa))rotate(x->fa),rotate(x);
	else rotate(x);
	if(to==NULL)rt=x;
}
void ins(node *&rt,int x,int y)//往rt这棵树里面插入y个x
{
	if(rt==NULL)
	{
		rt=new node(x,y,NULL);
		return;
	}
	node *p=rt->find(x);p->size+=y;
	if(x==p->x)p->tot+=y,splay(p,NULL,rt);
	else if(x>p->x)p->zuo=new node(x,y,p),splay(p->zuo,NULL,rt);
	else p->you=new node(x,y,p),splay(p->you,NULL,rt);
}
two getrank(node *&rt,int x)//找到rt这棵树中x的排名区间
{
	if(rt==NULL)return (two){0,0,true};
	node *p=rt->find(x);
	splay(p,NULL,rt);
	if(p->x==x)return (two){(p->zuo!=NULL?p->zuo->size:0)+1ll,(ll)(p->zuo!=NULL?p->zuo->size:0)+p->tot,false};
	//假如存在,那么左子树的大小就是比他大的数的个数,然后+1~tot就是排名区间
	else
	{
		//因为find函数有一个性质,如果x不存在,那么找到的数要么是他的前驱,要么是它的后继
		if(x>p->x)return (two){(p->zuo!=NULL?p->zuo->size:0),(p->zuo!=NULL?p->zuo->size:0),true};
		//假如比当前点大,也就是找到了后继,那么后继的左子树大小就是答案
		//因为显然x比他们都大(别忘了上面对two里面xy的定义)
		else
		{
			//假如是前驱,那么就要找后继
			if(p->you==NULL)return (two){p->size,p->size,true};//假如没有后继
			else
			{
				node *nxt=p->you;
				while(nxt->zuo!=NULL)nxt=nxt->zuo;
				splay(nxt,NULL,rt);
				return (two){nxt->zuo->size,nxt->zuo->size,true};
			}
		}
	}
}
struct nod{//线段树
	int l,r;
	nod *zuo,*you;
	node *root,*lazy;//两棵splay
	nod(int x,int y)
	{
		l=x,r=y;
		root=lazy=NULL;
		if(l<r)
		{
			int mid=l+r>>1;
			zuo=new nod(l,mid);
			you=new nod(mid+1,r);
		}
		else zuo=you=NULL;
	}
	void change(int x,int y,int z)//往集合区间x~y假如z这个数
	{
		if(l==x&&r==y)
		{
			ins(lazy,z,1);//lazy里面存的数实际上表示它管理的区间里面每一个集合都有一个这个数
			//所以用的时候要相应的翻倍
			return;
		}
		ins(root,z,y-x+1);
		int mid=l+r>>1;
		if(y<=mid)zuo->change(x,y,z);
		else if(x>=mid+1)you->change(x,y,z);
		else zuo->change(x,mid,z),you->change(mid+1,y,z);
	}
	two ask(int x,int y,int z)
	{
		two re=getrank(lazy,z);//利用标记永久化求解
		if(re.x!=0ll)
		{
			if(!re.v)re.x=(re.x-1ll)*(ll)(y-x+1)+1ll;//假如有出现,那么要-1再翻倍最后+1
			else re.x*=(ll)(y-x+1);//如果没出现,直接翻倍即可
			re.y*=(ll)(y-x+1);//y无论怎样都是直接翻倍
		}
		if(l==x&&r==y)return re+getrank(root,z);
		int mid=l+r>>1;
		if(y<=mid)return zuo->ask(x,y,z)+re;
		else if(x>=mid+1)return you->ask(x,y,z)+re;
		else return zuo->ask(x,mid,z)+you->ask(mid+1,y,z)+re;
	}
};
nod *root;
inline int max(int x,int y){return x>y?x:y;}

int main()
{
	scanf("%d %d",&n,&m);
	root=new nod(1,n);
	int left=1,right=0;
	int id,x,y;
	for(int i=1;i<=m;i++)
	{
		scanf("%d %d %d",&id,&x,&y);
		if(id==1)
		{
			int z;
			scanf("%d",&z);
			root->change(x,y,z);
			right=max(right,z);
		}
		else
		{
			ll z;
			scanf("%lld",&z);
			int l=left,r=right,ans=-1;
			while(l<=r)
			{
				int mid=l+r>>1;
				two re=root->ask(x,y,mid);//得到 mid 的排名区间 
				if(re.v)re.x++,re.y++;//此时的xy表示mid比多少个数大
				if(re.x<=z&&z<=re.y&&!re.v){ans=mid;break;}//假如找到了答案
				if(re.x<=z)r=mid-1;//否则继续二分
				else l=mid+1;
			}
			printf("%d\n",ans); 
		}
	}
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值