之前一直认为线段树可以解决划分树的所有问题。直到愚昧的我遇到poj2104,用线段树怎么也无从下手,于是下定决心学学划分树。以我的理解,划分树的主要功能是求区间的第k大元素,可能有的题要灵活做些修改。
背景
给定一个无序的数据序列,然后给定若干小区间,查询区间第k小的元素。单纯的暴力方法一般无法解决问题;一般的线段树也不能轻松解决问题。因此,此处有必要引进划分树这一数据结构。下面简单介绍一下我关于划分树的读书笔记。
划分树的基本思想
划分树的基本思想就是对于某个区间,把它划分成左右两个子区间,左边区间的元素小于等于右边区间的元素(不必排序且保证稳定性),在某一深度dep内toleft[dep][i]记录[1,i]分到左边的元素个数。查找时通过记录进入区间左子树的元素个数,确定下一个查找区间(此处有两重含义,下面做介绍),最后范围缩小到区间长度为1,此时已经找到该元素。
划分树有两个基本操作
1.构建
建树的过程比较简单,对于区间[l,r],首先通过对原数组的排序找到这个区间的中间元素a[mid],小于a[mid]的元素以及等于a[mid]且靠左的元素(包括a[mid])划入他的左子树[l,mid];其余的划入右子树[mid+1,r],有一约束就是保证正好存满左右子树,不能使哪边的存储元素个数超过它所能存储的上限。同时,对于区间[l,r]的第i个元素a[i],记录在当前深度dep下[1,i]区间内有多少元素被划入左子树(我这里记为 toleft[dep][i] ,这里是为了后面的查询搓澡的方便)。然后,对它的左子树区间[l,mid]和右子树区间[mid+1,r]递归的继续建树。
建树的时候要注意对于被分到同一子树的元素,元素间的相对位置不能改变,这就类似快速排序中的稳定性—相同元素间的相对顺序不变。
此过程时间复杂度为O(N*log(N))。
2.查询
查找时主要问题是确定将要查找的区间。假设在深度为dep,大区间[L,R]中查找小区间[l,r]中的第k小元素。
我们的想法是先确定小区间[l,r]中的第k小元素在大区间[L,R]的左右哪个子树中,然后修改对应的dep,[L,R],[l,r],k……直到找到l=r为止,a[l]即为所求。以上所做的都得根据
toleft[][]的值。
具体做法为toleft[dep][l-1]记录着当前深度dep下l左边进入左子树的元素个数,toleft[dep][r]记录着当前深度下r左边进入左子树的元素个数,下面就得讨论,记lsum=
toleft[dep][r]-toleft[dep][l-1],lsum为区间[l,r]内进入左子树的元素个数。
若lsum>=k,说明小区间[l,r]中的第k小元素在大区间[L,R]的左子树上。此时到大区间[L,R]的左子树上继续查找,但小区间[l,r]应作相应的修改,newl=L+ toleft[dep][l-1]-
toleft[dep][L-1],r=newl+lsum-1。
若lsum<k,说明小区间[l,r]中的第k小元素在大区间[L,R]的右子树上。此时到大区间[L,R]的右子树上继续查找,但小区间[l,r]应作相应的修改,newr=r+toleft[dep][R]-
toleft[dep][r],newl=newr-(r-l+1-lsum)+1=newr-(r-l-lsum),k=k-lsum。
若某时刻l=r,此时退出,a[l]即为所求。
此过程时间复杂度为O(N)。
小结
关于划分树,构建、查询操作都是比较基础的,大致跟线段树差不多,是线段树的某一特殊形式,采用二分思想,结点维护的是当前深度下从1到i这段区间内进入左边的元素个数(依我愚见划分树大都这样)。在实现的时候应该注意一些小细节,比如修改小区间[l,r]、k等,否则就会功败垂成。
以上就是我所理解的划分树的基本思想及两个基本操作。鉴于水平有限,错误难免,希望和大家一起学习交流,请斧正!