划分树详解

题目:给出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。所以这里要注意!!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值