划分树(模板+poj2104)

划分树的讲解转自于师哥的博客:https://blog.csdn.net/Akatsuki__Itachi/article/details/80030929

进入正题:

有这样一类题目,求的是区间内的第k大数。

划分树的定义就是对整体的区间进行划分,把相对于原来序列中较小的值放在左子树,较大的放在右子树,最后按照它的性质进行查询以此找到要查询的区间里的第k大数。

例图(图是偷的~~~)
这里写图片描述
1.建树
建树是一个不停递归的过程
第一步:首先我们要根据排序后的数组找到当前层数的中值(中值即中位数。注意,是中位数,不是中间的数),将没有排序的序列(即输入的原序列)里面的数这样安排:小于中位数的放进左子树,大于等于中位数的放进右子树。当然了,这是针对中值只有唯一一个时候的做法,一会再说多个中值应该怎么处理。

第二步:对于每一个子区间,我们都采用第一步的方法去划分,直到左右区间相等的时候,即为终止递归的条件。

第三步:在我们向左子树里放数的时候,我们还要统计出区间 [left,right ] 里有多少个数进入了左子树(这个主要用于查询操作)。

在划分树的时候,有几点需要注意:
1.建树是分层的,所以我们要用二维数组去存储,第一维只需要20就够了,因为100000的数据量的话,它的层数为logN。
2.划分的标准是中值,在第一步里已经特别强调过。
3.划分的数永远存放在它的下一层,为什么呢?下面举个例子模拟一下过程就知道了。

那么下面先列出我们要用到的数组:

const int MAXL(1e5);
int tree[20][MAXL+50];//第一维代表当前的树的层数,
                      //第二维代表这一层经过划分后的序列值
int toLeft[20][MAXL+50];//第一维代表当前的树的层数,
                     //第二维代表当前层区间[left,right]进入左子树的数目
int sorted[MAXL+50];//将初始序列排序后的数组

题目中的数据的原始序列是

4 2 5 7 1 8 3 6
排序后的序列为
1 2 3 4 5 6 7 8
那么我们tree [ 0 ]保存的应该是原始序列
并且得到toLeft [ 0 ] 的序列
tree[0] = 4 2 5 7 1 8 3 6
toLeft[0]= 1 2 2 2 3 3 4 4

再次强调一遍
toLeft [ i ] [ j ] 存的是 第 i 层,当前划分区间【 left , right 】里进入左子树的个数
至于为什么要这么存,一会说查询的时候就知道了。

模拟一下划分过程
首先是第一层,找到中值4 ( sorted[ ( left + mid) / 2 ] )
那么tree [ 1 ] 和toLeft [ 1 ] 应该是

tree[1]= 4 2 1 3 5 7 8 6
toLeft[1]= 0 1 2 2 1 1 1 2

可能这里有人注意到问题了,为什么把4划分到了左区间?上面不是说大于等于中值的划分到右区间吗? 别急-

第二层,分别对左子树和右子树按照上述的方法划分

tree[2]= 2 1 4 3 5 6 7 8
toLeft[2]= 0 1 0 1 1 1 1 1

在这里再啰嗦地解释一下这一组的toLeft数组
很明显这一组的 2 1 4 3 5 6 7 8
分别在左 右 左 右 子树
那么对于左子树里的 2 1这个小区间,进入下一层左子树的数分别为 0 1
对于右子树 4 3 这个小区间,进入下一层左子树的数分别为 0 1


第三层

tree[3]= 1 2 3 4 5 6 7 8
toLeft[3]= 0 0 0 0 0 0 0 0

下面开始说另外一个要注意的问题:有多个中值怎么办?

因为我们要使得左右区间的数量尽可能的均等
所以在这里,我们用一种特殊的处理方法。

在还没有进行划分之前,我们先假设中值左边的数据都小于中值。
即 设置一个suppose = mid - left + 1。
如果当前的数小于中值,就使suppose减一,即

if(tree[level][i]<sorted[mid]
    suppose--;

如果结果如我们假设的那样,那么suppose最后一定等于1,否则,就说明中值的数量不唯一。那么在下面进行的时候,如果还剩suppose>1,就先把中值放在左子树,直到suppose为0,如果仍还有中值,就把剩下的放进右子树。
通过这样操作,就能均分左右子树了。

再举个例子增深理解:
3 3 4 4 4 5 7
中值为4,左子树要放4个((1+7)/2),右子树放3个
处理后的suppose为2
那么遇到第一个4,放进左子树,suppose=1;
遇到第二个4,放进左子树,suppose=0;
遇到第三个4,这时suppose已经等于0,所以放进右子树。

在建好树之后,接下来就是查询的问题。
假设初始大区间为【left , right】,要查询的区间为【qLeft , qRight】
现在要查询区间【qLeft , qRight】的第k大数

我们的想法是,先判断【qLeft , qRight】在【left , right】的哪个子树中,然后找出对应的小区间和k,然后递归查找,直到小区间qLeft==qRight时为止。

那如何解决这个问题呢?这时候前面记录的进入左子树的元素个数就派上用场了。通过之前的记录可以知道,在区间【left , qLeft】中有toLeft [ level ] [ qLeft - 1 ] 个元素进入了左子树,记它为lef,同理,在区间【left , qRight】中有toLeft [ level ] [ qRight ] 个元素进入了左子树,记它为rig , 所以在区间【qLeft , qRight】之间就有 rig - lef 个元素进入了左子树,记为 toLef。 如果 toLef>= k ,说明 第k大元素肯定进入了左子树,那么就进入左子树查找,否则进入右子树查找。

那么接下来要解决确定小区间的问题:

如果进入的是左子树,那么小区间就应该是
【 left +( [ left,qLeft-1] )进入左子树的数目,left +( [ left,qRight ] )进入左子树的数目-1】
即:【 left + lef , left + lef + tolef-1 】,并且,这时候k的值不用变化。

如果进入的是右子树,那么小区间就应该是
【 mid +( [ left,qLeft-1] )进入右子树的数目+1,mid +( [ left,qRight ] )进入右子树的数目】
即:【 mid + qLeft - left -lef + 1 , mid + qRight - left - toLef - lef + 1 】
同时,这里的k要发生变化,变为k-(【qLeft , qRight】进入左子树的元素个数)
即 k-toLef

其中mid = ( left + right ) / 2

这里的区间式子很长,需要仔细思考。

下面举个例子(又是偷的图~~~)
这里写图片描述
以下是模板题
poj2104

#include<cstdio>
#include<cstring>
#include<cstdlib>
#include<cmath>
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
#define MAX 100000
int tree[20][MAX+50];     //第一维存原值,后来的存划分后的左右子树的值
int toLeft[20][MAX+50];   //代表当前层区间[left,right]进入左子树的数目
int sorted[MAX+50];       //排序好的数组
void build_tree(int level,int left,int right)
{
    if(left==right)
        return ;
    int mid=(left+right)>>1;
    int suppose=mid-left+1;
    for(int i=left; i<=right; i++)
        if(tree[level][i]<sorted[mid])
            suppose--;
    int subLeft=left,subRight=mid+1;
    for(int i=left; i<=right; i++)
    {
        if(i==left)
            toLeft[level][i]=0;
        else
            toLeft[level][i]=toLeft[level][i-1];
        if(tree[level][i]<sorted[mid]||tree[level][i]==sorted[mid]&&suppose>0)
        {
            tree[level+1][subLeft++]=tree[level][i];
            toLeft[level][i]++;
            if(tree[level][i]==sorted[mid])
                suppose--;
        }
        else
            tree[level+1][subRight++]=tree[level][i];
    }
    build_tree(level+1,left,mid);
    build_tree(level+1,mid+1,right);
}
int query(int level,int left,int right,int qleft,int qright,int k)
{
     if(qleft==qright)
        return tree[level][qleft];
     int mid=(left+right)>>1;
     int lef;    //[left,qleft]进入左子树的个数
     int tolef;  //[qleft,qright]进入左子树的个数
     if(qleft==left)
        lef=0,tolef=toLeft[level][qright];
     else
        lef=toLeft[level][qleft-1],tolef=toLeft[level][qright]-lef;
     if(k<=tolef)
     {
         int newleft=left+lef;   
         int newright=left+lef+tolef-1;  //加上个数再减去1
         return query(level+1,left,mid,newleft,newright,k);
     }
     else
     {
         int newleft=mid+qleft-left-lef+1; 
         //qleft-left-lef为查询区间前面那一段区间里的存到右子树的数的个数
         int newright=mid+qleft-left-lef+qright-qleft+1-tolef;
     //qright-qleft+1-tolef为[qleft,qright]区间内的存到右子树里的数的个数
         k-=tolef;
         return query(level+1,mid+1,right,newleft,newright,k);
     }
}
int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1; i<=n; i++)
    {
        scanf("%d",&tree[0][i]);
        sorted[i]=tree[0][i];
    }
    sort(sorted+1,sorted+n+1);
    build_tree(0,1,n);
    for(int i=0;i<m;i++)
    {
        int l,r,k;
        scanf("%d%d%d",&l,&r,&k);
        int ans=query(0,1,n,l,r,k);
        printf("%d\n",ans);
    }
    return 0;

}

猜你喜欢

转载自blog.csdn.net/qq_41700151/article/details/81484518