题目:给出n个数,和m个区间 ,对于每个区间 [ l , r ] ,输出这个区间里面第k大的数。
我们来看看划分树是怎么构造的。^_^
构造
以2104为例子,举2 0 6 8 5 1 4 3 9为例子,构造划分树是这样的
上面的内容是用一个二位数组val存储下来的,第一维表示划分树的第几层,第二位表示在这一层中的位置。要注意的是,每一层不仅仅存储一个子树,就像上图中的2 0 6 8 5 1 4 3 9派生的左右子树2 0 1 4 3和6 8 5 9是在同一层的。另外我们还要存储下整体排序好的序列,记为st。
构造划分树的时候,对于每一个区间,我们找出这个区间的中值mid。是中值,不是中间的值。然后我们让小于中值的所有数划分到左子树,让大于中值的数都到右子树,注意在子树里面维持原来的相对次序!如果只有一个中值,因为中值位于(left+right)>>1,而这个位置是属于左区间的,所以划分到左子树。但如果有不止一个中值这就不好办了。
这里我们先讨论只有一个中值我们怎么实现上面的步骤。区间 [ left , right ] 位于cen层的话,两个子区间就位于cen+1层,而且左右相邻,合起来跟原来的区间等长的,左区间起点是left,右区间起点是 (left+right)/2+1,由此,一开始弄两个变量ll,rr指向这两位置,在原来的区间从左往右扫描,遇到小于等于中值的,就放到左区间变量 ll 指向的地方,变量 ll 后移一位。右区间的类似移动变量 rr。这样就可以啦。
然后我们在讨论又不止一个中值的情况。我们在st对应的[left,right]里面找出这个区间的左子区间里面有多少个跟中值相同的,发现有x个。那么也就是说到时候会有x个中值要放到左子树。我们也像上面一样扫描,扫到中值本身,我们直接放到左子树,如果左子树的中值已经放满了x个,那就改放到右子树。
上面说到的是划分过程。你会发现划分的最后,最后一层的序列跟st是一模一样的。
另外,我们在构造划分树的时候,我们还要存储下,从区间起点left开始到当前点,一共有多少个点被放到了右子区间,这个点本身是否被放到了右子区间。这个是方便后面查询用的。如下图紫色的数字。
查询
在x的区间 [ x.left , x.right ] 查询子区间 [ left , right ] 的第k大值
首先算出[ left , right ] 里面有多少个数(记为dif)被分到了左子树,利用上图紫色数字记录的信息。
如果dif<=k,说明这个第k大值在下一层是被分到了右子树,然后我们就到右子树找,否则就到左子树找。
在右子树找,就是到2x的区间里面找,我们就需要算出我们在2x区间里面是要查找哪个区间的第几大值。这是要重新算出来的。我们可以利用我们记录的信息查出来。
代码:
#include<stdio.h>
#include<algorithm>
#define N 100100
using namespace std;
struct node
{
int left,right,mid;
}a[N*4];
struct Tree//tree是记录划分的每一层的结果
{
int val;//当前点的值
int num;//区间起点到该点之间有多少个点被移动到了右子区间
int p;//是否被移动到了右子区间
}tree[20][N];//注意2^20要大于N
int st[N];
void build(int left,int right,int cen,int t)
{
int m;
Tree *last=tree[cen-1],*cur=tree[cen];
//last上一层,cur当前层
a[t].left=left;
a[t].right=right;
m=a[t].mid=(left+right)>>1;
int mid=st[m],sum=0,j,ll=left,rr=m+1;
//mid保存的时候区间[left,right]的中值,建树最重要的是处理好重复的中值要放的位置。
//当然,如果数字没有重复是很好做的。
for(j=m;j>=left;j--)
{
if(st[j]==mid)
sum++;
else
break;
}
//记录下这个区间的左子区间里面有多少个重复的中值。
//也就是重复的中值有多少个要摆到左子区间去。
for(j=left;j<=right;j++)
{
int v=last[j].val;
if(v==mid)
{
if(sum)//首先遇到的sum个重复中值摆到左边
{
cur[ll++].val=mid;
last[j].p=last[j].num=0;
sum--;
}
else
{
cur[rr++].val=mid;
last[j].p=last[j].num=1;
}
}
else if(v<mid)
{//小于中值的摆到左子区间,摆的时候维持原来的相对次序
cur[ll++].val=v;
last[j].p=last[j].num=0;
}
else
{
cur[rr++].val=v;
last[j].p=last[j].num=1;
}
}
for(j=left+1;j<=right;j++)
{ //这样可以累计下从left开始到当前元素中有多少个被移到了右子树中去
last[j].num+=last[j-1].num;
}
if(left==right)
return;
int temp=t<<1;
build(left,m,cen+1,temp);
build(m+1,right,cen+1,temp+1);
}
int query(int left,int right,int k,int cen,int t)
{
int mid=a[t].mid;
Tree ll=tree[cen][left],rr=tree[cen][right];
if(a[t].left==a[t].right)
return ll.val;
int dif=(right-left+1)-(rr.num-ll.num+ll.p),temp=t<<1;
//dif记录的时候区间[left,right]有多少个去了左子区间,dif=总个数-去右子区间的个数
if(dif>=k)
{
return query(left-ll.num+ll.p,right-rr.num,k,cen+1,temp);
//更新在左子区间查询的是[left-ll.num+ll.p,right-rr.num]里面的第k大值
//(看前面有多少个到了右边)
}
else
{
return query(mid+ll.num+1-ll.p,mid+rr.num,k-dif,cen+1,temp+1);
//更新右子区间查询[mid+ll.num+1-ll.p,mid+rr.num]里面的第k-dif大值
//(看来了右边的有多少个,加上起点mid)
}
}
int main()
{
int n,m,i,j,k;
while(scanf("%d%d",&n,&m)!=EOF)
{
for(i=1;i<=n;i++)
{
scanf("%d",&st[i]);
tree[0][i].val=st[i];
}
sort(st+1,st+1+n);
build(1,n,1,1);
while(m--)
{
scanf("%d%d%d",&i,&j,&k);
printf("%d\n",query(i,j,k,0,1));
}
}
return 0;
}
注意2104是肯定没有重复数字的,但是2761是肯定有的。不考虑的话WA死你。
这里我补充一下为什么有重复中值会很麻烦,就像数据
INPUT:
4 1
1 8 6 6
1 4 4
OUTPUT:
8
如果我们构造的时候不小心考虑,如果一股脑的把全部中值都放到左子区间,会溢出,过程:(中值是6)遇到1我们放在下一层的第一位,遇到8我们发到下一层的第3位,遇到6我们放在下一层的第2位,遇到第四个6,我们就顺势把这个6放到了第3位了,这样就覆盖了原来的8。所以这里要注意!!