初学划分树,小见解之!POJ-2104/HDU-2665

时间:2024-01-02 21:02:32

划分树

本来是学主席树的,可怜我等巨弱观群巨博客难解fotle主席的思想精髓。于是学了一下划分树,嗯,花了一下午时间理解build(其实自己模拟一遍就通了),我很难理解为什么划分树会看不懂而能学会主席树。唉,学业有先后术业有专攻,斯已矣。其实思想不难理解,代码的话找个样例模拟一遍然后就通了。

+++划分树+++。

本人比较懒省事于是就用了二维数组式的风格。

1.sorted[MAXN]。原数组排序后的数组(参照下图第一行)

2.tree[20][MAXN]。20是划分树的层数,每层最多MAXN个元素,也就是数列的长度。可以想想为什么最多20层就够了。如果不明白,别急,往下看!

3.t_l[20][MANX]。即toleft,t_l[id][i]表示第id层的当前区间的第i个位置之前有多少个元素进入了左子树,当前区间的其余元素进入了右子树。如果你是初学的话,我知道这有点难懂,不过别急,继续。。。

4. 先来看一幅图:

初学划分树,小见解之!POJ-2104/HDU-2665

就以这幅图进行详解:    //如果以下感觉看的很烦不妨直接用下面代码手推一遍上例,再回来看,会有惊喜的哟!

原数组可以当做第0层,第1层也行,看个人的代码习惯了。上面第三条提到有的元素进入了左子树,那以什么为划分标准呢?可以看到最上面是排序后的数组。原数组一分为二,左子树都是比4小的,于是划分标准:区间[l,r],比中间值sorted[(l+r)/2]小的都分到了左子树,其余的都在右子树。为什么要这样呢,其实可以对比归并排序和快排,实质是一样的。第0层[1,8],比sorted[4]=4小的都分在了左子树.你可能要问如果与中间值相等怎么办呢,这个下面再讨论。还记得t_l[0][i]吗,第0层的当前区间的第i个位置之前有多少个被分到了左子树(比sorted[mid]小--参照划分标准),t_l[0][1]=1,t_l[0][2]=1,t_l[0][3]=1,t_l[0][4]=2....我们要求区间第k大其实就是根据这个来求的。

上面讲到讲上层区间[l,r]一分为二,一半进入左子树,我们设置个lc=l,rc=mid+1,那么比sorted[mid]元素小的或等的肯定进入左子树,于是if(tree[id][i]<sorted[mid]) tree[id+1][lc++]=tree[id][i];if(tree[id][i]>sorted[mid]) tree[id+1][rc++]=tree[id][i]。看懂了吗,就是将上层区间小于等于中间值的一半为左子树,另一半为右子树,然后递归对这两个小的区间进行build(),类似归并排序,为什么是类似呢,因为这个过程是自上而下的,与归并排序相反。说到这里,可以这样理解:每层最多MAXN个元素,下一层就是这个区间按照划分标准分为两半分别为左右子树,然后递归下一层进行相同的操作,然后区间越分越小,左后达到叶子节点return
;这个过程并没有像线段树那样产生新的两个节点,而是相同大小的区间在每一层被越分越小。t_l[id][i]就储存了每个区间比sorted[mid]小的个数,我们在查询区间第k小的时候只需判断t[p].num[b]-t[p].num[a-1]>=k,如果满足说明进入左孩子的个数已经超过k个,那么就往左孩子里面查找同时更新查找区间。

    不知道你是不是更加稀里糊涂了,原谅我的表达能力有限,网上有很多博客,其实个人认为找几篇写得比较好的理解了思想然后模拟一下代码基本上就完全明白了--我的经历。

来看一下建树: //咳咳,划分树精髓,敲黑板划重点啦~!

先定义变量:

const int N=1e5+5;

int  tree[20][N], t_l[20][N];

int  a[N];//懒省事的我没有用sorted作为数组名。

void build(int l,int r,int id)
{
if(l==r) return ;
int mid=(l+r)/2;
int same=mid-l+1;
/* same用来标记和中间值val_mid相等的,且分到左孩子的数的个数
初始时,假定当前区间[lft,rht]有mid-lft+1个和valu_mid相等。
先踢掉比中间值小的,剩下的就是要插入到左边的 */
for(int i=l; i<=r; i++) if(tree[id][i]<a[mid]) same--;
int lc=l,rc=mid+1;
for(int i=l; i<=r; i++)
{
if(tree[id][i]<a[mid]) tree[id+1][lc++]=tree[id][i];
else if(tree[id][i]==a[mid]&&same) tree[id+1][lc++]=tree[id][i],same--;
else tree[id+1][rc++]=tree[id][i];
t_l[id][i]=t_l[id][l-1]+lc-l;
}
build(l,mid,id+1);
build(mid+1,r,id+1);
}

每个区间的初始值最好先全部赋为0。

如果你明白了上面的代码t_l[id][i]的含义也就理解的更加透彻了。

假设要查询区间[a,b]的第k小(网上很多博客都是说第k大,当我真正明白了这个划分树的时候其实查询的是第k小。后面会有两个例题讲解)如果tree[p].num[b]-tree[p].num[a-1]>=k,说明在左子树。如果和你想的不一样,或许你还没真正明白build()过程。t[p].num[b]-t[p].num[a-1]其实就是表示区间[a,b]之间有多少数进入了左子树(被分到了左边),按照划分标准被分到左边都是比中间值小的,如果大于等于k不就说明区间[a,b]内至少有k个比中间值小的,那么我们查询的就是第k小那么再递归下去查找即可。如果不满足呢,则在右子树嘛,只是这是的k要减去t[p].num[b]-t[p].num[a-1]再往右子树查找。

来看query()函数:

//[l,r]表示要查询的小区间,[L,R]表示大区间。
int query(int l,int r,int k,int L,int R,int id)
{
if(l==r) return tree[id][l];//到达了叶子节点
int mid=(L+R)/2;
int cnt=t_l[id][r]-t_l[id][l-1];
if(cnt>=k)
{
int newl=L+t_l[id][l-1]-t_l[id][L-1];//更新查询区间
int newr=newl+cnt-1;
return query(newl,newr,k,L,mid,id+1);
}
else
{
int newr=r+t_l[id][R]-t_l[id][r];
int newl=newr-(r-l-cnt);
return query(newl,newr,k-cnt,mid+1,R,id+1);
}
}

全篇我并没有花篇幅去细说query(),本菜其实理解了build()函数和query()函数查询第k小的大概,而区间更新那里我并没有去模拟这个过程,借用某大牛的话:其实你去模拟一下也就明白了。因为以后可能还会忘,时间原因我就没有太纠于细节(全靠模板.jgp)

以上如果有问题的话请联系我,或者有更好的想法也请联(教)系(教)我!

来看POJ-2104和HDU-2665。两道题代码几乎一致,划分树裸模板入门题:

n个数的序列,m次询问每次给出区间和查询大小k。

void build(int l,int r,int id)
{
if(l==r) return ;
int mid=(l+r)/2;
int same=mid-l+1;
for(int i=l; i<=r; i++) if(tree[id][i]<a[mid]) same--;
int lc=l,rc=mid+1;
for(int i=l; i<=r; i++)
{
if(tree[id][i]<a[mid]) tree[id+1][lc++]=tree[id][i];
else if(tree[id][i]==a[mid]&&same) tree[id+1][lc++]=tree[id][i],same--;
else tree[id+1][rc++]=tree[id][i];
t_l[id][i]=t_l[id][l-1]+lc-l;
}
build(l,mid,id+1);
build(mid+1,r,id+1);
}
int query(int l,int r,int k,int L,int R,int id)
{
if(l==r) return tree[id][l];
int mid=(L+R)/2;
int cnt=t_l[id][r]-t_l[id][l-1];
if(cnt>=k)
{
int newl=L+t_l[id][l-1]-t_l[id][L-1];
int newr=newl+cnt-1;
return query(newl,newr,k,L,mid,id+1);
}
else
{
int newr=r+t_l[id][R]-t_l[id][r];
int newl=newr-(r-l-cnt);
return query(newl,newr,k-cnt,mid+1,R,id+1);
}
}
int main()
{
int n,m;
while(~scanf("%d%d",&n,&m))
{ memset(t_l,0,sizeof(t_l));
for(int i=1; i<=n; i++)
{
scanf("%d",&tree[0][i]);
a[i]=tree[0][i];
}
sort(a+1,a+1+n);
build(1,n,0);
while(m--)
{
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",query(l,r,k,1,n,0));
}
} return 0;
}//代码精(A)简(C)才是王道!

昨晚突发奇想:如果会划分树还要学主席树吗?

狂搜问题求解答,在某吧里得到答案:划分树适用静态区间离线查询,主席树也能做,归并树也行。而如果涉及修改数列,则用树套树(主席树+树状数组),听起来好厉害的样子,可惜本弱还不会主席树!