POJ 2104 题解 ——划分树
本文是一个系列的第一篇文章。本系列基于上交老师出品的《程序设计解题策略》书籍。该书介绍了49个ACM/ICPC算法设计策略。本系列打算用一个更简单的方式介绍书中的解题策略。当然,针对每个策略,也会给出相应的测试题目及其代码。
划分树算法
问题与复杂度
划分树主要应用于求解整数区间内第k大的数。相关的题目如POJ 2104。划分树整个算法主要分为两个部分:第一部分,建立划分树——时间复杂度为O(nlogn),空间复杂度为O(nlogn);第二部分,查找——每次查找的时间复杂度为O(logn)。其中,n表示输入到数组的数据的大小。
建树
划分树的逻辑结构 划分树的逻辑结构是一个(平衡)二叉树,如图1所示。每个结点是一个一位数组。当前结点的两个左右子节点分别存储当前结点中各一半的数据,并且左子结点数组中的所有数据都小于右子结点数组中的所有数据。要注意的是,相对位序是不变的,如图中所示。
建立划分树,就是使用递归的方法把上面的这个结构给构建出来。下面我们考虑使用什么样的存储结构来表示划分树。
存储结构 由于每个结点是一个数组,因此我们可以使用二维数组来模拟划分树,可表示tree[logN][N],其中N表示n的最大值。二维数组的第一维表示二叉树的某层,第二维表示某层上的结点。此外,我们还需要一个附加记录toleft[logN][N]来辅助后面的查找操作。toleft[depth][i]表示:在第depth层,从数组toleft[depth]的第1个位置到第i个位置中共有多少个元素被划分到了下一层的左子结点中。
建树代码 从数据存储角度看,建树过程就是对二维数组tree与toleft进行数据填充的过程。C++代码如下:
void buildPartitionTree(int l, int r, int depth){
// build partition tree.
if(l == r) return;
int mid = (l+r)>>1;
int leftEquMid = mid - l + 1; // the number of equal to A[mid] that will be partitioned to left
for(int i=l; i<=r; i++){
if(tree[depth][i] < A[mid]){
// A is a array that be sorted by ascending order.
leftEquMid--;
}
}
int lpos=l, rpos= mid + 1;
for(int i=l; i<=r; i++){
if(tree[depth][i] < A[mid]){
tree[depth+1][lpos++] = tree[depth][i];
}else if(tree[depth][i] == A[mid] && leftEquMid > 0){
tree[depth+1][lpos++] = tree[depth][i];
leftEquMid--;
}else{
tree[depth+