划分树,类似线段树,主要用于求解某个区间的第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[]的值就可以了。
查询:
查询是在每一层的toleft的基础上进行区间的缩小,直到待查询区间缩小为1即为查询结果
总区间为[L,R],待查区间为[l,r];k是第K大值,toleft[r]-toleft[l-1]为区间[l,r]内被分配到左子数的个数.
toleft[r]-toleft[l-1]>=k
说明第k大值一定在左子树此时就可以更新区间,首先大区间二分为[L,L+R>>1],然后考虑小区间,可以确定的是,[l,r]分配在左子树的元素一定在区间[L,L+R>>1]内,所以,确定左边界sl=L+toleft[l-1]-toleft[L-1]:toleft[l-1]-toleft[L-1]为[L,l-1]内被分配到左子树的个数,他们不在查找之列,但相对位置不变,这些元素一定排在前面,以此来确定左边界,右边界就好确定了,因为toleft[r]-toleft[l-1]>=k,所以左边界加上toleft[r]-toleft[l-1]-1即可,即sr=sl+toleft[r]-toleft[l-1]-1,同时必有L<=sl,sr<=L+R>>1;
toleft[r]-toleft[l-1]<k
此时,元素在右子树,二分大区间[L+R>>1|1,R],确定右边界,sr=r+toleft[R]-toleft[r],因为相对位置不变,toleft[R]-toleft[r]是分配在左子树的必定会往前移动,所以右边界往后移动,右边界确定后,确定左边界sl=sr-(l-r-toleft[l-1]-toleft[L-1]),减去分配到左子树的,剩下就在右子树。注意,此时要更新k k=k--toleft[l-1]-toleft[L-1],已经确定前面有toleft[l-1]-toleft[L-1]比其小,所以就是求右子树内区间[sl,sr]第k--toleft[l-1]-toleft[L-1]小。