划分树求区间第k值
前言:
关于区间最值问题的求解,我们一般采用线段树来维护区间最值,得到了O(NlogN)的算法。但对于区间第k值问题,我们应该如何解决呢?本文将介绍一种基于线段树思想衍生出来的新结构——划分树,来解决这个问题。
1 划分树
其实,划分树和线段树的区别并不大,可以归纳为两点:
1.1 划分树每一个区间[L,R]维护的是一个一维数组[L..R]。
1.2 划分树每一个根结点[L,R]中前[(R-L+1)/2]小的点组成左子树,后[(R-L+1)/2]大的点组成右子树。
2 划分树求区间k值
我们首先修改一下思考方式,原问题变成在已经sorted的数列中找出第k个在区间[s,t]中的数字。
先给出一个例子,8个数字分别为{6 3 7 2 4 1 8 5},于是我们的根为:
6 3 7 2 4 1 8 5
之后选出前4小的作为左子树,选出后4大的作为右子树。
3 2 4 1 | 6 7 8 5
但是在左子树和右子树中数字的顺利依然按照原序列中的顺序。
考察询问[3,7]中第2小的数字,那么对应根中:
6 3 7 2 4 1 8 5
我们发现红色区域中有3个数字{2,4,1}都属于左子树,那么说明红色区间中前3小的数字都在左子树:
3 2 4 1 | 6 7 8 5
于是下一步我们只需要在左子树中查找第2小的数字就可以了。
我们令操作Find(l,r,s,t,k,deep)表示在第deep层的[l,r]区间中查找[s,t]区间中的第k值。那么例子中的操作为Find(1,8,3,7,2,0),而第二次在左子树中的操作就是Find(1,4,2,4,2,1)。
那么如何才能确定子树中的操作Find(l’,r’,s’,t’,k’,deep+1)呢?对于结点,我们记录suml[i]记录当前区间中前i个数字有几个被分配到左子树。那么cnt=suml[t]-suml[s-1]就是查询区间[s,t]中到左子树的个数。
如果cnt>=k,那么下一次的查找必然在左子树中,s’和t’分别为l+sum[s-1]-sum[l-1]和l+sum[t]-sum[l-1]-1,k’=k,而[l’,r’]=[l,mid]。
如果cnt<k,那么下一次查找必然在右子树总,s’和t’分别为mid+(s-l+1)-(sum[s-1]-sum[l-1])和mid+(t-l+1)-(sum[t]-sum[l-1]),k=k-cnt,而[l’,r’]=[mid+1,r]。
直到区间[l,r]满足l=r,那么返回原序列中第l个数字。
3 时间复杂度分析
每次我们区间缩小一半,时间复杂度O(NlogN),如果用二分找s’和t’时间复杂度就是O(NlogNlogN)。
4 Code
最后给出查询部分的代码:
int query( int s , int t , int k , int l , int r , int dep ) { if ( s == t ) return node[dep][s] ; int mid = (l + r) >> 1 ; int x = lsum[dep][s - 1] - lsum[dep][l - 1] ; int y = lsum[dep][t ] - lsum[dep][l - 1] ; int rx = s - l - x , ry = t - l + 1 - y , cnt = y - x ; if ( cnt >= k ) return query(l+x , l+y-1 , k , l , mid , dep+1 ) ; else return query(mid+1+rx, mid+ry, k-cnt, mid+1, r, dep+1 ) ; }
划分树,就是每个结点代表一个序列,设这个序列的长度为len,若len>1,则序列中前len/2(上取整,这里数学公式不好打,真囧)个元素被分到左子结点,左子结点代表的序列就是这些元素按照根结点中的顺序组成的序列,而剩下的元素被分到右子结点,右子结点代表的序列也就是剩下的元素按照根结点中的顺序组成的序列;若len=1,则该结点为叶结点。划分树最重要的应用就是找区间第K小(只是查找,不包括修改)。
写划分树时主要有两个函数:建树和找区间第K小。由于后者AHdoc神犇已经总结了,所以这里只总结建树的函数。
设目前结点为[l..r](l<r,就是目前的结点是原序列不断划分后得到[l..r]这一段,其实也就是a0[l..r]打乱顺序后得到的,a0为原序列递增排序后的序列)首先找到中值,就是a0[mid],mid=l+r>>1。然后可以得到,[l..r]中小于中值的的元素一定被划分到左子结点,[l..r]中大于中值的元素一定被划分到右子结点,而[l..r]中等于中值的元素则不确定,有的被划分到左子结点,有的被划分到右子结点,这就需要先找到应被划分到左子结点的等于中值的元素个数sm(从mid开始不断往左,直到找到边界处或者找到一个小于中值的元素为止,或者说,sm就是a0[l..mid]中等于中值的元素个数),然后开始划分,小于中值分到左子结点,大于中值分到右子结点,等于中值的,若目前还没满sm则分到左子结点否则分到右子结点。另外中间有两个值需要记录(找区间
int mkt(int l,int r,int d){ T[++index].l = l, T[index].r = r; int mid = (l+r)>>1; T[index].mid = mid; if(l == r) return index; int midv = a0[mid],sm = mid-l+1; for(int i = mid; i >= l; i--){ if(a0[i] != a0[mid]){ sm--; } } int x = l, y = mid+1; for(int i = l; i <= r; i++ ){ if(a[d][i] < midv){ a[d+1][x++] = a[d][i]; sl[d][i] = sl[d][i-1]+1; } else if(a[d][i] == midv && sm){ a[d+1][x++] = a[d][i]; sl[d][i] = sl[d][i-1]+1; sm--; } else{ a[d+1][y++] = a[d][i]; sl[d][i] = sl[d][i-1]; } } int p = index; T[p].lchild = mkt(l,mid,d+1); T[p].rchild = mkt(mid+1,r,d+1); return p; }
第K小时必须要用到):sl和sr。sl[i]表示[l..i]中被分到左子结点的元素个数,sr[i]表示[l..i]中被分到右子结点的元素个数(这里l<=i<=r。显然sl[i]+sr[i]=i-l+1,其实sr[i]可以不用记录的,这里只是为了在找第K小操作中减少计算次数,起到空间换时间的作用)。
建树代码: