划分树讲解(转)

       划分树,类似线段树,主要用于求解某个区间的第k 大元素(时间复杂度log(n)),快排本也可以快速找出,但快排会改变原序列,所以每求一次都得恢复序列。

  下面就以 POJ 2104 进行解说:

  题目意思就是,给你n 个数的原序列,有m 次询问,每次询问给出l、r、k,求原序列l 到r 之间第k 大的数。n范围10万,m范围5千,这道题用快排也可以过,快排过的时间复杂度n*m,而划分树是m*logn(实际上应该是nlogn才对,因为建图时间是nlogn,n又比m大),分别AC后,时间相差很明显。

  划分树,顾名思义是将n 个数的序列不断划分,根结点就是原序列,左孩子保存父结点所有元素排序后的一半,右孩子也存一半,也就是说排名1 -> mid的存在左边,排名(mid+1) -> r 的存在右边,同一结点上每个元素保持原序列中相对的顺序。见下图:

   

  红点标记的就是进入左孩子的元素。

  当然,一般不会说每个结点开个数组存数,经观察,每一层都包含原本的n 个数,只是顺序不同而已,所以我们可以开val[20][N]来保存,也就是说共20层,每一层N个数。

  我们还需要一个辅助数组num,num[i]表示i 前面有多少数进入左孩子(i 和i 前面可以弄成本结点内也可以是所有,两种风格不同而已,下面采取的是本结点内),和val一样,num也开成num[20][N],来表示每一层,i 和i 前面(本结点)有多少进入左孩子。

  第一层:1 进入左孩子,num[1]=1,5 进入右孩子,num[2]=1,...,num[8]=4。

  第二层:5 进入左孩子,num[5]=1,6 进入右孩子,num[6]=1,...,num[8]=2。

  建图时就是维护每一层val[]和num[]的值就可以了。

建树:

const int MAXN=1e5+100;
int tree[30][MAXN];          //tree[i][j]第i层的元素排放,第0层为输入的原数组
int num[30][MAXN];           //num[i][j]表示第i层第j个元素前面有多少个点进入下一层的左区间
int sorted[MAXN];            //存排好序的原数组
void Build(int l,int r,int level)
{
    if(l==r)
        return ;
    int mid=(l+r)/2,isame=mid-l+1;        //isame保存有多少和sorted[mid]一样大的数进入下一层左区间
                                        
                                          //初始值为该层左区间中的数的数量
    for(int i=l;i<=r;i++)
    {
        if(tree[level][i]<sorted[mid])    //如果遇到小于sorted[mid]的数,isame--
            isame--;                    
    }                                     //执行完该步后,isame中存的便是和sorted[mid]一样大的数的数量                              
    int sl=l,sr=mid+1;
    for(int i=l;i<=r;i++)
    {
        if(l==i)
            num[level][i]=0;      
        else
            num[level][i]=num[level][i-1];
        if(tree[level][i]<sorted[mid]||tree[level][i]==sorted[mid]&&isame>0)
        {
            num[level][i]++;
            tree[level+1][sl++]=tree[level][i];
            if(tree[level][i]==sorted[mid])
                isame--;
        }
        else
            tree[level+1][sr++]=tree[level][i];
    }
    Build(l,mid,level+1);
    Build(mid+1,r,level+1);
}

 查询时,比如要查找2 到6 之间第3 大的数,那么先判断2 到6 之间有多少元素进入左子树,(在此忽略细节)num[6]-num[2-1]=2,就说明2 到6 有两个数进入左子树,又因为我们要找的是第3 大的数,所以一定在右子树中。可以算出,下标2 前面有0 个数进入右子树,所以2 到6 之间进入右子树的元素在下一层一定是从5 开始排的,2 到6 的区间进入右子树3 个,所以下一层从5 排到7 都是原本2 到6 之间的。现在,因为去左子树两个,所以现在要在右子树找5 到7 之间排名第1 的元素。在下一层的查找步骤和第一层一样,也就是不断递归,当跑到叶子结点时就可以返回正确的值了。

查询:

int query(int l,int r,int left,int right,int k,int level)
{
    if(left==right)
        return tree[level][left];
    int preleft;                  //[l,left-1]进入到左区间的数的数量
    int laleft;                   //[left,right]进入到左区间的数的数量
    if(l==left)
    {
        preleft=0;
        laleft=num[level][right];
    }
    else
    {
        preleft=num[level][left-1];
        laleft=num[level][right]-preleft;
    }
    int mid=(l+r)/2;
    if(laleft>=k)
    {
        int newl=preleft+l;
        int newr=newl+laleft-1;
        query(l,mid,newl,newr,k,level+1);

    }
    else
    {
        int newl=mid+1+(left-l-preleft);  // left-l 表示left前面有多少数,再减preleft表示这些数中分到右区间的有多少个
        int newr=newl+(right-left+1-laleft)-1; // right-left+1 表示left到right有多少数,减去分到左边的,剩下是分到右边的,下标处理,要减去1
        query(mid+1,r,newl,newr,k-laleft,level+1);
    }
}

 

讲解转自大佬: hchlqlz

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值