划分树,顾名思义就是将一个序列,划分成很多小部分。其作用就是可以快乐地找到给定区间第K大的数。其实使用归并树和快排也都可以找到区间内第K大的数,但其效率都不如划分树要好,快排过的时间复杂度O(n x m),而划分树是O(n x logn)。
划分树原理:
1. 建树:根结点就是原序列,左孩子保存父结点所有元素排序后的一半,右孩子也存一半,也就是说排名1 -> mid的存在左边,排名(mid+1) -> r 的存在右边,同一结点上每个元素保持原序列中相对的顺序,蓝色数字表示其是在左子树中的,最后通过递归左右子树建立整棵树,同时在建树的过程中记录一个辅助数组num[i],其意义是从 l 到 i 其中小于中间值的元素个数。
(来自hchlqlz博客)
图示:
2. 查询:对于在区间[l,r]中的第k大的询问,先比较num[r]- num[l-1] (在区间 [l,r] 中在左子树中的元素个数) 和k的值。
如果num[r]- num[l-1] >= k (左子树中的个数大于k) [l,r] 中的第k大的数字在左子树,到左子树中去找。
否则第k大的元素就在右子树。此处要注意递归到左右子树时, l,r ,k 值的下一状态。
举个栗子:
比如要查找2 到6 之间第3 大的数,那么先判断2 到6 之间有多少元素进入左子树,(在此忽略细节)num[6]-num[2-1]=2,就说明2 到6 有两个数进入左子树,又因为我们要找的是第3 大的数,所以一定在右子树中
划分树步骤:
1.建树
可以发现,划分树的每一层结点都是n个,所以采用一个tree[max][max]的二维数组来存储整棵树,同样num数组也采用同样的方式存储。
a.将原数组sort后,存在一个数组sorted中,在每个区间中找到区间中间值mid(偏左)
b.扫描原数组在区间[l,r]的部分,将l到r中的小于等于mid的数组个数记录下来在num数组中
c.将小于等于mid的树放到左子树,将大于mid的放到右子树
d.向左、右子树开始递归建树 递归基:r == l 时,返回
2.查询区间[l,r]中第k大的元素
if(k > num[r]-num[l-1]) 进入右子树
if(k <= num[r]-num[l-1]) 进入左子树
下面以POJ 2014为例 给出代码 http://poj.org/problem?id=2104
题意:给出n个数,进行m询问,每次询问给出l,r,k 找出区间[l,r]的第k大的数。
代码:
/*
1.建树
a.将原数组sort后,存在一个数组sorted中,在每个区间中找到区间中间值mid
b.扫描原数组在区间[l,r]的部分,将l到r中的小于等于mid的数组个数记录下来在num数组中
c.将小于等于mid的树放到左子树,将大于mid的放到右子树
d.向左、右子树开始递归建树
递归基:r == l 时,返回
2.查询区间[l,r]中第k大的元素
判断num[r]-num[l-1]与k值的大小
a.if(k > num[r]-num[l-1]) 进入左子树
if(k <= num[r]-num[l-1]) 进入右子树
Tip:1.注意查询递归转移的s,e,l,r的参数更新变化
2.注意每次区间中num数组的要初始化
*/
#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
const int Max = 100010;
int n,m,mid_sort,last,ans= 0 ;
int a[Max],sorted[Max];
int tree[20][Max],num[20][Max] = {0};
void creat(int l,int r,int layer){
//递归基
if(l == r) {
last = layer;
return ;
}
//找到中间点的位置
int mid = (l+r)>>1;
mid_sort = sorted[mid];
//左右子树的起始位置
int cnt_l = l-1,cnt_r = mid;
//cnt_same:当前结点可以容纳多少个和mid_sort一样的元素的上限
//cnt_same值为l到mid的元素个数减去l到r中小于mid_sort的元素个数
int cnt_same = mid-l+1;
for(int i = l; i <= r; i++) if(tree[layer][i] < mid_sort) cnt_same--;
for(int i = l; i <= r; i++){
//注意每次区间中num数组的要初始化
if(i == l) num[layer][i] = 0;
else num[layer][i] = num[layer][i-1];
//元素小于中间值mid_sort 或者 等于mid_sort且没到达上限 放到左子树
if(tree[layer][i] < mid_sort || (tree[layer][i] == mid_sort && cnt_same > 0)) {
num[layer][i]++;
tree[layer+1][++cnt_l] = tree[layer][i];
if(tree[layer][i] == mid_sort)
cnt_same--;
}
else {
tree[layer+1][++cnt_r] = tree[layer][i];
}
}
creat(l,mid,layer+1);
creat(mid+1,r,layer+1);
}
//s代表当前区间的左界,e代表当前区间的右界,layer为树的层数 ,l是查询的左界,r是查询右界,k是题中定义
int query(int s,int e,int layer,int l,int r,int k){
//递归基
if(s == e) {return tree[layer][s];}
//ll表示 l 前面有多少元素进入左孩子
int ll ;
if(l != s) ll= num[layer][l-1];
else ll = 0;
//toleft表示左孩子里有多少元素
int toleft;
int mid = (s+e)>>1;
toleft = num[layer][r] - ll;
//cout<<"num[layer][r] = "<<num[layer][r]<<" num[layer][l-1] = "<<num[layer][l-1]<<" s = "<<s<<" e = "<<e<<" layer = "<<layer<<" l = "<<l <<" r = "<<r<<" k = "<<k<<" toleft = "<<toleft<<"\n";
if(toleft >= k)
//s+ll是掠过s之前(不包含s)的被分到左孩子里的元素,s+num[layer][r]-1也是相似意思
return query(s,mid,layer+1,s+ll,s+num[layer][r]-1,k);
else{
// l-s 表示l前面有多少数,再减ll 表示这些数中去右子树的有多少个
int lr = mid+1+(l-s-ll);
// r-l+1 表示l到r有多少数,减去去左边的,剩下是去右边的,去右边1个,下标就是lr,所以减1
//因为k 大于 去左孩子的元素个数,所以就是找左孩子中第k-toleft大的数
return query(mid+1,e,layer+1,lr,lr+r-l+1-toleft-1,k-toleft);
}
}
int main(){
scanf("%d %d",&n,&m);
int l,r,k;
for(int i = 1; i <= n; i++) scanf("%d",a+i),tree[1][i] = sorted[i] = a[i];
sort(sorted+1,sorted+n+1);
creat(1,n,1);
for(int i = 0; i < m; i++){
scanf("%d %d %d",&l,&r,&k);
printf("%d\n",query(1,n,1,l,r,k));
}
return 0;
}