poj2104 K-th Number(主席树)

同一道题开两篇

主席树:


可用于查询:一个数列{a1,a2,…,an}在给定区间[L,R]内 各元素(即所有a1,a2,…,an)的出现次数 

这个数据结构由n棵形状相同的线段树构成,第i棵线段树记录:前缀区间[1,i]内 各元素的出现次数 
要先将a[]离散化,得到离散化后元素的范围1~m。而线段树的每个节点就对应其中一段子区间,节点值记录区间内所有元素出现次数之和 
也就是说:线段树i上的子区间[i,j]之和,就是前缀区间[1,i]内a[i],a[i+1],…,a[j]的出现次数之和 

建树:
树i就是在树i-1的基础上,将每一个对应a[i]所在区间的点的权值加1。
比如:离散化后,元素的范围是1~8,a[5]=3,那么树5就是 将树4上对应[1,8],[1,4],[3,4],[3,3]的点权值加1 得到的 
因此,所有线段树形状完全相同,只是各节点权值不同,树i上的>=树i-1上的 
但是建n棵线段树,空间会达到O(n^2)。解决方法:考虑每一个a[i],只会导致树i有log(n)个结点与树i-1不同,因此我们只新建这log(n)个即可 
效果图是树i有很个点都"贴"在之前的树上 

找到区间[L,R]:
既然每棵树代表前缀区间,且每棵线段树形状完全相同,那么树R与树L-1各节点对应相减得到的树就是记录[L,R]内各元素出现次数的线段树 

询问[L,R]内第k小:在树R减去树L-1得到的树上二分即可 


#include<stdio.h>
#include<stdlib.h>
int a[100005]={0},b[100005]={0},c[100005]={0},root[100005]={0},ls[2000000]={0},rs[2000000]={0},sum[2000000]={0};
//ls[x]/rs[x]:点x的左/右孩子编号(若a[1]进入树i-1的右孩子,则树i与树i-1左孩子编号相同), sum[x]:出现次数和 
int n,sz=0;//sz所有线段树总节点数 
void jh(int* a,int* b)
{
	int t=*a;
	*a=*b;
	*b=t;
}
void kp(int low,int high)
{
	int i=low,j=high,mid=a[(i+j)/2];
	while(i<j)
	{
		while(a[i]<mid) i++;
		while(a[j]>mid) j--;
		if(i<=j)
		{
			jh(&a[i],&a[j]);
			jh(&c[i],&c[j]);
			i++;
			j--;
		}
	}
	if(j>low) kp(low,j);
	if(i<high) kp(i,high);
}
void tj(int left,int right,int x,int& y,int v)
{
	int mid=(left+right)/2;
	y=++sz;//树i与树i-1在此节点权值不同,此节点需新建 
	sum[y]=sum[x]+1;//对应区间内某个数出现次数+1,导致该点的权值+1
	if(left==right) return;
	ls[y]=ls[x];//这一步不能省去:递归下一层要么为左,要么为右,不可能两个都走,没走到的那个节点编号y树与x树相同,在这里赋值 
	rs[y]=rs[x];
	if(v<=mid) tj(left,mid,ls[x],ls[y],v);
	else tj(mid+1,right,rs[x],rs[y],v);
}
int cx(int l,int r,int k)
{
	int x=root[l-1],y=root[r],left=1,right=n,mid;
	while(left<right)
	{
		mid=(left+right)/2;
		if(sum[ls[y]]-sum[ls[x]]>=k)//左 
		{
			right=mid;
			x=ls[x];
			y=ls[y];
		}
		else//右 
		{
			left=mid+1;
			k-=sum[ls[y]]-sum[ls[x]];
			x=rs[x];
			y=rs[y];
		}
	}
	return left;
}
int main()
{
	int m,i,l,r,k;
	scanf("%d%d",&n,&m);
	for(i=1;i<=n;i++)
	{
		scanf("%d",&a[i]);
		c[i]=i;
	}
	kp(1,n);
	for(i=1;i<=n;i++)//离散化 
		b[c[i]]=i;
	for(i=1;i<=n;i++)
		tj(1,n,root[i-1],root[i],b[i]);
	for(;m>0;m--)
	{
		scanf("%d%d%d",&l,&r,&k);
		printf("%d\n",a[cx(l,r,k)]);
	}
	return 0;
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值