学主席树的时候看到一篇博客说是因为一个大牛当时不会划分树而想出的另一种解决区间第k小的问题,所以在学了主席树之后我就学了下划分树。
划分树解决静态区间第k小问题比主席树的时空消耗都要少,不过好像不能解决动态区间第k小问题。
转载自:http://www.cnblogs.com/hchlqlz-oj-mrj/p/5744308.html
这篇博客写的非常好,所以我直接转载了。
划分树
划分树,类似线段树,主要用于求解某个区间的第k 大元素(时间复杂度log(n))。
下面以hdu2665为例进行讲解,给你n 个数的原序列,有m 次询问,每次询问给出l、r、k,求原序列l 到r 之间第k小的数。(n,m<=100000)
划分树,顾名思义是将n 个数的序列不断划分,根结点就是原序列,左孩子保存父结点所有元素排序后的一半,右孩子也存一半,也就是说排名1 -> mid的存在左边,排名(mid+1) -> r 的存在右边,同一结点上每个元素保持原序列中相对的顺序。见下图:
红点标记的就是进入左孩子的元素。
当然,一般不会说每个结点开个数组存数,经观察,每一层都包含原本的n 个数,只是顺序不同而已,所以我们可以开val[20][N]来保存,也就是说共20层,每一层N个数。
我们还需要一个辅助数组num,num[i]表示i 前面有多少数进入左孩子(i 和i 前面可以弄成本结点内也可以是所有,两种风格不同而已,下面采取的是本结点内),和val一样,num也开成num[20][N],来表示每一层,i 和i 前面(本结点)有多少进入左孩子。
第一层:1 进入左孩子,num[1]=1,5 进入右孩子,num[2]=1,…,num[8]=4。
第二层:5 进入左孩子,num[5]=1,6 进入右孩子,num[6]=1,…,num[8]=2。
建图时就是维护每一层val[]和num[]的值就可以了。
int h[maxn]; //排序后数组
int val[20][maxn]; //20层,每一层元素排放,0层就是原数组
int num[20][maxn]; //num[i] 表示i前面有多少个点进入左孩子
void build(int l,int r,int deep)
{
if(l==r) return;
//cnt保存有多少和h[mid]一样大的数进入左孩子,用于保证最多只有一半的数进入左孩子
int mid = (l+r)>>1,cnt = mid-l+1;
for(int i=l;i<=r;i++) if(val[deep][i]<h[mid]) cnt--;
int ln=l,rn=mid+1; //本结点两个孩子结点的开头,ln左
for(int i=l;i<=r;i++)
{
if(i==l) num[deep][i] = 0;
else num[deep][i] = num[deep][i-1];
if(val[deep][i]<h[mid] || val[deep][i]==h[mid]&&cnt>0)
{
val[deep+1][ln++] = val[deep][i];
num[deep][i]++;
if(val[deep][i]==h[mid]) cnt--;
}
else
{
val[deep+1][rn++] = val[deep][i];
}
}
build(l,mid,deep+1);
build(mid+1,r,deep+1);
}
查询时,比如要查找2 到6 之间第3 大的数,那么先判断2 到6 之间有多少元素进入左子树,(在此忽略细节)num[6]-num[2-1]=2,就说明2 到6 有两个数进入左子树,又因为我们要找的是第3 大的数,所以一定在右子树中。可以算出,下标2 前面有0 个数进入右子树,所以2 到6 之间进入右子树的元素在下一层一定是从5 开始排的,2 到6 的区间进入右子树3 个,所以下一层从5 排到7 都是原本2 到6 之间的。现在,因为去左子树两个,所以现在要在右子树找5 到7 之间排名第1 的元素。在下一层的查找步骤和第一层一样,也就是不断递归,当跑到叶子结点时就可以返回正确的值了。
int query(int deep,int s,int e,int l,int r,int k)
{
if(l==r) return val[deep][l];
int pre; //pre表示s前面有多少元素进入左孩子
if(l==s) pre=0; //这里要特判一下和左端点重合时
else pre = num[deep][s-1];
//这一层s到e之间进入左子树的有cnt个
int mid = (l+r)>>1,cnt = num[deep][e]-pre;
if(k<=cnt)
{
return query(deep+1,l+pre,l+num[deep][e]-1,l,mid,k);
}
else
{
// s-l 表示s前面有多少数,再减pre 表示这些数中去右子树的有多少个
int rn = mid+1+s-l-pre;
// e-s+1 表示s到e有多少数,减去去左边的,剩下是去右边的,去右边1个,下标就是rn,所以减1
return query(deep+1,rn,rn+e-s+1-cnt-1,mid+1,r,k-cnt);
}
}
hdu2665
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn = 1e5+5;
int n,m;
int h[maxn],val[20][maxn],num[20][maxn];
void build(int l,int r,int deep)
{
if(l==r) return;
//cnt保存有多少和h[mid]一样大的数进入左孩子,用于保证最多只有一半的数进入左孩子
int mid = (l+r)>>1,cnt = mid-l+1;
for(int i=l;i<=r;i++) if(val[deep][i]<h[mid]) cnt--;
int ln=l,rn=mid+1; //本结点两个孩子结点的开头,ln左
for(int i=l;i<=r;i++)
{
if(i==l) num[deep][i] = 0;
else num[deep][i] = num[deep][i-1];
if(val[deep][i]<h[mid] || val[deep][i]==h[mid]&&cnt>0)
{
val[deep+1][ln++] = val[deep][i];
num[deep][i]++;
if(val[deep][i]==h[mid]) cnt--;
}
else
{
val[deep+1][rn++] = val[deep][i];
}
}
build(l,mid,deep+1);
build(mid+1,r,deep+1);
}
int query(int deep,int s,int e,int l,int r,int k)
{
if(l==r) return val[deep][l];
int pre; //pre表示s前面有多少元素进入左孩子
if(l==s) pre=0; //这里要特判一下和左端点重合时
else pre = num[deep][s-1];
//这一层s到e之间进入左子树的有cnt个
int mid = (l+r)>>1,cnt = num[deep][e]-pre;
if(k<=cnt)
{
return query(deep+1,l+pre,l+num[deep][e]-1,l,mid,k);
}
else
{
// s-l 表示s前面有多少数,再减pre 表示这些数中去右子树的有多少个
int rn = mid+1+s-l-pre;
// e-s+1 表示s到e有多少数,减去去左边的,剩下是去右边的,去右边1个,下标就是rn,所以减1
return query(deep+1,rn,rn+e-s+1-cnt-1,mid+1,r,k-cnt);
}
}
int main()
{
int t;
scanf("%d",&t);
while(t--)
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&val[0][i]),h[i]=val[0][i];
sort(h+1,h+1+n);
build(1,n,0);
int l,r,k;
while(m--)
{
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",query(0,l,r,1,n,k));
}
}
return 0;
}
看一下和主席树的对比
划分树AC
主席树AC