原网址:划分树详解
对4 5 2 8 7 6 1 3 分别建划分树和归并树
划分树如下图
红色的点是此节点中被划分到左子树的点。
我们一般用一个结构体数组来保存每个节点,和线段树不同的是,线段树每个节点值保存一段的起始位置和结束位置,而在划分树和递归树中,每个节点的每个元素都是要保存的。为了直观些,我们可以定义一个结构体数组,一个结构体中保存的是一层的元素和到某个节点进入左子树的元素的个数,不同于线段树,我们不能保存一个节点的起始结尾位置,因为随层数的增加,虽然每个结构体保存的元素数目是一定的,但随层数的增加,元素早已被划分到不同的子树中了,而且这数目是指数增加的。
那我们如何确定一个子树的边界? 在划分树中,我们都是采用递归的方式进行访问的,如果一个节点的边界是(l,r),假设mid = (l+r )/2,那么他的左右子树的边界恰好是(l,mid)和(mid+1, r),然后在进行下一层的递归。
const int maxn = 100000 + 10; typedef struct node { int num[maxn]; int cnt[maxn]; }tree[20];
至于这里为什么将tree大小定义20!和线段树一样,划分树都是完全完全二叉树,叶子节点的深度相差不会超过1,而且所有非叶子节点都有左右子树。关于划分树的题目,我们遇到的数据量一般都是10^5,也就是说如果把这些数建成树的话深度不会超过20。
我们看图片会发现划分树有以下几个特点。
1,树的根节点是原来的数组,没有做任何处理。
2,和快排有些类似,每个节点的子节点(如果有)中,左节点的所有元素都有小于右节点的所有元素,前提是原数组中无重复的数,关于存在重复的元素的情况,我们会详细讨论。
3,如果我们从左到有取出所有叶子节点,每个叶子节点的元素(肯定只有一个元素),这些元素必然是有序的。
我们以poj 2104 k-th number 为例,详细说一下划分树的建造和查询。
//poj 2104 //2013-04-15-19.47 #include <stdio.h> #include <algorithm> const int maxn = 100005; using namespace std; int sor[maxn]; struct node { int num[maxn]; int cnt[maxn]; }tree[20];
接下来是建树的函数,建树之前,将数组放树的第一层,当做根节点,然后将原数组进行排序(至于升降视情况而定,但在整个程序中要统一)放在另外一个数组中,我这里放在sor中。我们先讨论集合中没有重复元素的情况,先找出mid(当前节点的中间位置),然后从左到右遍历所有元素,如果小于等于sor[mid] 放入左子树,否则放入右子树,然后递归创建左右子树。
如果其中包含相同元素,而且恰好sor[mid] 左右都有与他相同的元素,这样我们就不能仅仅依靠比较大小来决定该数的去向了。当然,小于sor[mid]的必然去左子树,这是毋庸置疑的,但对于相同的元素,我们有个巧妙的处理方法,先计算在有序数组中sor[mid]左边有多少个和sor[mid]的元素,比如说有x个,然后在建树过程中将出现的前x+1个和sor[mid]相等的放入左子树,其他如右子树即可,然后递归建立左右子树。
另外还有非常重要的一部分,不是还有一个cnt[]数组吗,这个数组是划分树的核心部分,它的作用是记录这个节点到第i个元素有多少被划分到了左子树,看代码很容易理解。
建树代码如下
void buildtree(int l, int r, int d) { if (l == r) { return; } int mid = (l+r)>>1; int oplift = l, opright = mid+1; //对左右子树的操作位置的初始化 int same_as_mid = 0; /*用来计算在mid左边有多少个和sor[mid]相同的数(包括mid),这些都要放到左子树*/ for (int i = mid; i > 0; i--) { if (sor[i] == sor[mid]) same_as_mid++; else break; } int cnt_lift = 0; for (int i = l; i <= r; i++) { if (tree[d].num[i] < sor[mid]) { tree[d+1].num[oplift++] = tree[d].num[i]; cnt_lift++; tree[d].cnt[i] = cnt_lift; } else if(tree[d].num[i] == sor[mid] && same_as_mid) { tree[d+1].num[oplift++] = tree[d].num[i]; cnt_lift++; tree[d].cnt[i] = cnt_lift; same_as_mid--; } else { tree[d].cnt[i] = cnt_lift; tree[d+1].num[opright++] = tree[d].num[i]; } } buildtree(l, mid, d+1); buildtree(mid+1, r, d+1); }
关于询问操作,假如我们询问从区间[ql,qr]中第k大的数(看清具体要求,有些是第k小数),然后我们通过cnt数组,确定[ql,qr]有多少个点进入了左子树,然后判断第k数在左还是右子树,然后递归查询。
肯定有m = cnt[qr] - cnt[ql-1]个数进入了左子树,如果ql是节点的左边界的话就有cnt[qr]个数进入左子树了,这里要额外注意。如果m <= k,,进入左子树查询第k数,否则进入右子树查询k-m数,我们还要确定在子树中查询的边界, 只是一个比较难理解的地方。
int query(int l, int r, int d, int ql, int qr, int k) //6个参数,分别是当前节点的左右边界、深度、询问的左右边界及k值 { if (l == r) return tree[d].num[l]; int mid = (l+r)>>1; int sum_in_lift, lift; if (ql == l) { sum_in_lift = tree[d].cnt[qr]; lift = 0; } else { sum_in_lift = tree[d].cnt[qr] - tree[d].cnt[ql-1]; // 区间进入左子树的总和 lift = tree[d].cnt[ql-1]; } if (sum_in_lift >= k) //证明要找的点在左子树 { int new_ql = l+lift; int new_qr = l+lift+sum_in_lift-1; return query(l, mid, d+1, new_ql, new_qr, k); /*这里有必要解释一下,我们要确定下一步询问的位置,如果在ql的左边有i个进入左子树, 那么ql到qr中第一个进入左子树的必定在l+i的位置*/ } else { int a = ql - l - lift; int b = qr - ql + 1 - sum_in_lift; int new_ql = mid + a + 1; int new_qr = mid + a + b; return query(mid+1, r, d+1, new_ql, new_qr, k - sum_in_lift); } }