划分树,类似线段树,主要用于求解某个区间的第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);
}
}