划分树是一种用线段树作为辅助的数据结构,主要用于在lgn时间内查询区间的第k大数值,其原理是基于快排和归并排序的。
划分树的建树过程可以看做是模拟快排的过程,根节点保存原始数组的信息,同时再另开一个数组保存对原始数组排序好后的信息,然后通过排序好后的数组得到中位数,以中位数为划分依据,将小于中位数的数字划入左孩子,大于中位数的数字划入右孩子,那么等于中位数的数字该如何处理呢?其实既然以中位数为划分依据,就是将中位数及其左边的数划入右边,其它划入左边,保持左右孩子的数字的数目大致相同(可能相差1),因此可以用一个变量记录左边需要划入多少个与中位数相同的数字(即代码中的same),划入后剩下的自然就划入右边。
通过上面可以看出,划分树是分层结构,故用二维数组保存,通常20层就足够了,能应付10W左右的数据。代码如下:
const int MAXN=100010;
int tree[30][MAXN];//表示每层每个位置的值
int sorted[MAXN];//已经排序的数
int toleft[30][MAXN];//toleft[p][i]表示第i层从1到i有多少个数分入左边
void build(int l,int r,int dep)
{
if(l==r)return;
int mid=(l+r)>>1;
int same=mid-l+1;//表示等于中间值而且被分入左边的个数
for(int i=l;i<=r;i++)
if(tree[dep][i]<sorted[mid])
same--;
int lpos=l;
int rpos=mid+1;
for(int i=l;i<=r;i++)
{
if(tree[dep][i]<sorted[mid])//比中间的数小,分入左边
tree[dep+1][lpos++]=tree[dep][i];
else if(tree[dep][i]==sorted[mid]&&same>0)
{
tree[dep+1][lpos++]=tree[dep][i];
same--;
}
else //比中间值大分入右边
tree[dep+1][rpos++]=tree[dep][i];
toleft[dep][i]=toleft[dep][l-1]+lpos-l;//从1到i放左边的个数
}
build(l,mid,dep+1);
build(mid+1,r,dep+1);
}
划分树最麻烦的是查询,因为求一个区间的第k值,再往下查询时,要算出查询结果在孩子结点中所在的区间,具体见下图
//查询区间第k大的数,[L,R]是大区间,[l,r]是要查询的小区间
int query(int L,int R,int l,int r,int dep,int k)
{
if(l==r)return tree[dep][l];
int mid=(L+R)>>1;
int cnt=toleft[dep][r]-toleft[dep][l-1];//[l,r]中位于左边的个数
if(cnt>=k)
{
//L+要查询的区间前被放在左边的个数
int newl=L+toleft[dep][l-1]-toleft[dep][L-1];
//左端点加上查询区间会被放在左边的个数
int newr=newl+cnt-1;
return query(L,mid,newl,newr,dep+1,k);
}
else
{
int newr=r+toleft[dep][R]-toleft[dep][r];
int newl=newr-(r-l-cnt);
return query(mid+1,R,newl,newr,dep+1,k-cnt);
}
}