浅谈权值线段树

历届女足世界杯

浅谈权值线段树

BFqwq

·

2020-01-10 22:49:56

·

算法·理论

浅谈权值线段树

(tips:如果未学习过线段树不建议阅读)

众所周知,在信息学中,有一种神奇的数据结构叫做线段树,它可以解决许许多多的区间动态查询问题。

的确,线段树是一类神奇的数据结构。但是,如果你认为它只能解决一些区间动态查询的问题,那么就太低估它了。

(感谢犇犇犇犇提供的修改建议)

0 目录

什么是权值线段树

权值树的重要性质

权值树的辅助操作

权值树的习题精选

1 什么是权值线段树

顾名思义,权值线段树,就是对权值作为维护对像而开的线段树,即每个点上存的是区间内的对应数字的某种值(最常见的是出现次数)。

举个最简单的例子,权值线段树可以用于维护一个数在一个序列中出现的次数。

比如现在有一个数组1 ,1, 2, 2, 2, 3, 4, 5 ,6,7,8

对于每个节点,初始时个数为0

插入1

插入2

最后

(以上也就是一个建树的过程)

由于权值树也是线段树,所以权值树的操作也都是跟线段树一样的,时间复杂度是 \log len 每次操作。

(其中,len 代表最大的权值,因为我们是对权值开的线段树)

听到这儿,也许有人会想到平衡树。的确,权值树与平衡树的用途基本是相同的,同样的复杂度,同样支持动态。

但是,权值树代码量小,易于调整,优势也就由此显现出来了。

由于没有专门的模版题,我们就借用平衡树的模版来解决。

【模版】普通平衡树

code:(由于我们只需要关心每个数之间的相对关系,因此需要用到离散化操作。 )

#include

using namespace std;

inline int read(){

register int x=0;

register bool f=0;

register char c=getchar();

while(c<'0'||c>'9'){

if(c=='-') f=1;

c=getchar();

}

while(c>='0'&&c<='9'){

x=(x<<3)+(x<<1)+c-48;

c=getchar();

}

return f?-x:x;

}

void write(int x){

if(x<0) putchar('-'), x=-x;

if(x>=10) write(x/10);

putchar('0'+x%10);

}

const int maxn=111111;

struct seg{

int v;

}t[maxn<<3];

void pushup(int o){

t[o].v=t[o<<1].v+t[o<<1|1].v;

}

void change(int o,int l,int r,int q,int v){

if(l==r){

t[o].v+=v;

return ;

}

int mid=l+r>>1;

if(q<=mid) change(o<<1,l,mid,q,v);

else change(o<<1|1,mid+1,r,q,v);

pushup(o);

}

int query_rnk(int o,int l,int r,int ql,int qr){

if(ql<=l && r<=qr){

return t[o].v;

}

int mid=l+r>>1,ans=0;

if(ql<=mid) ans+=query_rnk(o<<1,l,mid,ql,qr);

if(qr>mid) ans+=query_rnk(o<<1|1,mid+1,r,ql,qr);

return ans;

}

int query_num(int o,int l,int r,int q){

if(l==r){

return l;

}

int mid=l+r>>1;

if(t[o<<1].v>=q) return query_num(o<<1,l,mid,q);

else return query_num(o<<1|1,mid+1,r,q-t[o<<1].v);

}

int lsh[maxn<<2],tot,n;

struct _node{

int opt,val;

}node[maxn<<2];

int main(){

n=read();

for(int i=1;i<=n;i++){

node[i].opt=read();

node[i].val=read();

if(node[i].opt==4) continue;

lsh[++tot]=node[i].val;

}

sort(lsh+1,lsh+tot+1);

tot=unique(lsh+1,lsh+1+tot)-lsh-1;

for(int i=1;i<=n;i++){

if(node[i].opt!=4) node[i].val=lower_bound(lsh+1,lsh+tot+1,node[i].val)-lsh;

if(node[i].opt==1) change(1,1,tot,node[i].val,1);

if(node[i].opt==2) change(1,1,tot,node[i].val,-1);

if(node[i].opt==3){

if(node[i].val==1){

puts("1");

continue;

}

printf("%d\n",query_rnk(1,1,tot,1,node[i].val-1)+1);

}

if(node[i].opt==4){

printf("%d\n",lsh[query_num(1,1,tot,node[i].val)]);

}

if(node[i].opt==5){

int rk=query_rnk(1,1,tot,1,node[i].val-1);

printf("%d\n",lsh[query_num(1,1,tot,rk)]);

}

if(node[i].opt==6){

int rk=query_rnk(1,1,tot,1,node[i].val)+1;

printf("%d\n",lsh[query_num(1,1,tot,rk)]);

}

}

return 0;

}

其中,change 是单点修改,query\_rnk 就是一个正常的区间求和。

而 query\_num 是权值树特有的操作,也就是查询第 q 大。

其操作原则就是:如果左子树有大于 q 个数个从左子树查询,否则查询右子树并减去左子树的个数之和。

其原理就是二分,如果小于等于 mid 的数超过 q 个,那么一定在左子树中,反之一定在右子树中。

对比两种算法:

上面的是平衡树(使用Splay),下面的是权值树。

显然,不管是时间,空间还是码量,都是权值树更优。

不过,权值树的空间优势仅在离线或是值域较小的情况下有优势。当值域较大时,由于不能动态开点,空间复杂度需要增加到 \operatorname O(V)(设 V 为值域)。

当然,如果使用压缩 trie 等方式,则可以将空间缩小为 \operatorname O(n),但在本文不作讨论。

2 权值树的重要性质

我们知道,对于一棵线段树而言,如果它的总长度不变,那么它的形态是不会改变的。

也就是说,在 len 不变的情况下,权值树的形态是不会改变的。

这样一来,我们就可以对权值树进行加减法操作。

对于权值树 A,B,若 A,B 形态相同,则我们可以直接合并这两棵权值树,合并的方式就是对应节点相加。

显然,加出来的树依然是一棵权值线段树(如图)。

同样的,权值树亦可以相减,减出来的树依然是权值树。

这个性质非常的重要,接下的的前缀和/树状数组就要基于这一性质。

3 权值树的辅助操作

前缀和

由于权值线段树的可合并性,因此,我们可以对权值线段树进行静态前缀和。

这样一来,我们就可以从全局问题升级到维护区间。

P3834 【模板】可持久化线段树 1(主席树)

这个题是一个典型的静态问题。我们可以直接使用权值树的前缀和解决。

对于每个点,建立一棵权值树,第 k 棵树维护 [1,k] 区间的出现次数。

然后查询操作就是这样的:

我们真正的需要的树为 [l,r] 的树(也就是要知道 [l,r] 区间内各数的出现次数 ),

而要得到这棵树,我们只需要将 [1,r] 的树与 [1,l-1] 的树相减即可。

(因为第 r 棵树维护 [1,r] 区间的出现次数,第 l-1 棵树维护 [1,l-1] 中的出现次数,两者相减就是我们想要的区间了)

如最上面那张图,我们要求第 6 小的数。

我们先查根节点,为 11,表示一共 11 个数。

左孩子权值为 7,表示 [1,4] 一共 7 个数,而我们需要第 6 小的数,7>6,所以我们要找的数一定在左孩子上。故递归左子树。

这时,我们发现左孩子为 5,所以前 5 小的数都在区间 [1,2] 上,所以我们要找的数在右孩子上。

因为左孩子已经有 5 个数了,我们要找右子树上的 6-5=1 小的数。故递归右子树。

同理递归,一直找到叶节点。我们发现找到了3。

然后再同区间第 k 大的操作即可。

int query(int o1,int o2,int l,int r,int q){

if(l==r){

return l;

}

int mid=l+r>>1,tmp=t[t[o2].ls].v-t[t[o1].ls].v;

if(tmp>=q) return query(t[o1].ls,t[o2].ls,l,mid,q);

else return query(t[o1].rs,t[o2].rs,mid+1,r,q-tmp);

}

但是,这个时候,一个新的问题出现了。

每一个权值树的空间都是 {\displaystyle Θ(n)} 级别的,那么总复杂度就是 Θ(n^2)。显然,这是无法接受的。

那么我们怎么解决呢?

动态开点

所谓动态开点,就是动态的分配内存。

显然,当我们修改一个权值时,只会对它和它的直属父辈节点产生影响(也就是一条链)。

那么,这个时候,我们只需要对这一条链进行分配内存,而剩下的节点可以借用原节点(如图所示)。

代码如下:

void pushup(int o){

t[o].v=t[t[o].ls].v+t[t[o].rs].v;

}

void change(int lsto,int &o,int l,int r,int q,int v){

if(!o) o=++cnt;

if(l==r){

t[o].v+=v;

return ;

}

int mid=l+r>>1;

if(q<=mid){

t[o].rs=t[lsto].rs;

t[o].ls=++cnt;

t[t[o].ls]=t[t[lsto].ls];

change(t[lsto].ls,t[o].ls,l,mid,q,v);

}

else{

t[o].ls=t[lsto].ls;

t[o].rs=++cnt;

t[t[o].rs]=t[t[lsto].rs];

change(t[lsto].rs,t[o].rs,mid+1,r,q,v);

}

pushup(o);

}

这样做之后,空间就会被降到 Θ(n logn) 级别,就可以接受了。

于是这题的代码如下:(一般对于 100000 的数据只需要开 32\times maxn 左右的空间即可,但不放心可以开的更大)

#include

using namespace std;

inline int read(){

register int x=0;

register bool f=0;

register char c=getchar();

while(c<'0'||c>'9'){

if(c=='-') f=1;

c=getchar();

}

while(c>='0'&&c<='9'){

x=(x<<3)+(x<<1)+c-48;

c=getchar();

}

return f?-x:x;

}

void write(int x){

if(x<0) putchar('-'), x=-x;

if(x>=10) write(x/10);

putchar('0'+x%10);

}

const int maxn=111111;

struct seg{

int v,ls,rs;

}t[maxn<<8];

int rt[maxn<<2],cnt;

void pushup(int o){

t[o].v=t[t[o].ls].v+t[t[o].rs].v;

}

void change(int lsto,int &o,int l,int r,int q,int v){

if(!o) o=++cnt;

if(l==r){

t[o].v+=v;

return ;

}

int mid=l+r>>1;

if(q<=mid){

t[o].rs=t[lsto].rs;

t[o].ls=++cnt;

t[t[o].ls]=t[t[lsto].ls];

change(t[lsto].ls,t[o].ls,l,mid,q,v);

}

else{

t[o].ls=t[lsto].ls;

t[o].rs=++cnt;

t[t[o].rs]=t[t[lsto].rs];

change(t[lsto].rs,t[o].rs,mid+1,r,q,v);

}

pushup(o);

}

int query(int o1,int o2,int l,int r,int q){

if(l==r){

return l;

}

int mid=l+r>>1,tmp=t[t[o2].ls].v-t[t[o1].ls].v;

if(tmp>=q) return query(t[o1].ls,t[o2].ls,l,mid,q);

else return query(t[o1].rs,t[o2].rs,mid+1,r,q-tmp);

}

int lsh[maxn<<2],tot,n,m,node[maxn<<2];

int main(){

n=read();m=read();

for(int i=1;i<=n;i++){

node[i]=read();

lsh[i]=node[i];

}

sort(lsh+1,lsh+n+1);

tot=unique(lsh+1,lsh+1+n)-lsh-1;

for(int i=1;i<=n;i++){

node[i]=lower_bound(lsh+1,lsh+tot+1,node[i])-lsh;

change(rt[i-1],rt[i],1,tot,node[i],1);

}

for(int i=1;i<=m;i++){

int l=read(),r=read(),q=read();

printf("%d\n",lsh[query(rt[l-1],rt[r],1,tot,q)]);

}

return 0;

}

不论是修改还是操作,时间复杂度都同线段树。

由于不需要建树,而每一次修改只需改一条链,所以每次消耗空间复杂度为 \log len,总空间复杂度 n\log len。

树状数组/线段树

由于权值树可以加减,那么我们也可以用树状数组或是线段树来维护。

在这里我就以树状数组为例:

我们先建立一个树状数组,以维护每一个节点的数据;

然后再在每个点建立权值树(配合动态开点)

P3380 【模板】二逼平衡树(树套树)

在修改的时候从树状数组找到需要修改的节点:

void add(int o,int v){

for(int i=o;i<=n;i+=lb(i)) change(rt[i],1,len,a[o],v);

}

然后在对应的权值树上进行修改:

void pushup(int o){

t[o].v=t[t[o].ls].v+t[t[o].rs].v;

}

void change(int &o,int l,int r,int k,int v){

if(!o) o=++tot;

if(l==r){

t[o].v+=v;

return ;

}

int mid=l+r>>1;

if(k<=mid) change(t[o].ls,l,mid,k,v);

else change(t[o].rs,mid+1,r,k,v);

pushup(o);

}

查询的时候也是一样,在此以查询第 k 小为例子

先从树状数组预处理出需要查询的节点。

此处我用 tem 记录需要加的节点,用 tmp 记录需要减去的节点。

int find_num(int l,int r,int k){

cnt=num=0;

for(int i=r;i;i-=lb(i)){

tem[++cnt]=rt[i];

}

for(int i=l-1;i;i-=lb(i)){

tmp[++num]=rt[i];

}

return query_num(1,len,k);

}

紧接着进行查询,具体做法与权值树相同:

int query_num(int l,int r,int k){

if(l==r) {

return l;

}

int mid=l+r>>1,sum=0;

for(int i=1;i<=cnt;i++) sum+=t[t[tem[i]].ls].v;

for(int i=1;i<=num;i++) sum-=t[t[tmp[i]].ls].v;

//统计左子树个数

if(k<=sum){

for(int i=1;i<=cnt;i++) tem[i]=t[tem[i]].ls;

for(int i=1;i<=num;i++) tmp[i]=t[tmp[i]].ls;

//所有节点全部进入左子树

return query_num(l,mid,k);

}

else{

for(int i=1;i<=cnt;i++) tem[i]=t[tem[i]].rs;

for(int i=1;i<=num;i++) tmp[i]=t[tmp[i]].rs;

//所有节点全部进入右子树

return query_num(mid+1,r,k-sum);

}

}

剩余的操作大致做法相同。

code:

#include

using namespace std;

inline int read(){

register int x=0;

register bool f=0;

register char c=getchar();

while(c<'0'||c>'9'){

if(c=='-') f=1;

c=getchar();

}

while(c>='0'&&c<='9'){

x=(x<<3)+(x<<1)+c-48;

c=getchar();

}

return f?-x:x;

}

const int maxn=50005;

int len=0;

const int inf=2147483647;

struct seg{

int v,ls,rs;

}t[maxn*100];

int rt[maxn],n,m,tot,tem[maxn],tmp[maxn],cnt,num;

int lsh[maxn<<1],a[maxn];

struct cz{

int a,b,c,d;

}q[maxn];

int lb(int x){

return x&(-x);

}

void pushup(int o){

t[o].v=t[t[o].ls].v+t[t[o].rs].v;

}

void change(int &o,int l,int r,int k,int v){

if(!o) o=++tot;

if(l==r){

t[o].v+=v;

return ;

}

int mid=l+r>>1;

if(k<=mid) change(t[o].ls,l,mid,k,v);

else change(t[o].rs,mid+1,r,k,v);

pushup(o);

}

void add(int o,int v){

for(int i=o;i<=n;i+=lb(i)) change(rt[i],1,len,a[o],v);

}

int query_num(int l,int r,int k){

if(l==r) {

return l;

}

int mid=l+r>>1,sum=0;

for(int i=1;i<=cnt;i++) sum+=t[t[tem[i]].ls].v;

for(int i=1;i<=num;i++) sum-=t[t[tmp[i]].ls].v;

if(k<=sum){

for(int i=1;i<=cnt;i++) tem[i]=t[tem[i]].ls;

for(int i=1;i<=num;i++) tmp[i]=t[tmp[i]].ls;

return query_num(l,mid,k);

}

else{

for(int i=1;i<=cnt;i++) tem[i]=t[tem[i]].rs;

for(int i=1;i<=num;i++) tmp[i]=t[tmp[i]].rs;

return query_num(mid+1,r,k-sum);

}

}

int find_num(int l,int r,int k){

cnt=num=0;

for(int i=r;i;i-=lb(i)){

tem[++cnt]=rt[i];

}

for(int i=l-1;i;i-=lb(i)){

tmp[++num]=rt[i];

}

return query_num(1,len,k);

}

int query_rnk(int l,int r,int k){

if(l==r) {

return 0;

}

int mid=l+r>>1,sum=0;

if(k<=mid){

for(int i=1;i<=cnt;i++) tem[i]=t[tem[i]].ls;

for(int i=1;i<=num;i++) tmp[i]=t[tmp[i]].ls;

return query_rnk(l,mid,k);

}

else{

for(int i=1;i<=cnt;i++) sum+=t[t[tem[i]].ls].v,tem[i]=t[tem[i]].rs;

for(int i=1;i<=num;i++) sum-=t[t[tmp[i]].ls].v,tmp[i]=t[tmp[i]].rs;

return sum+query_rnk(mid+1,r,k);

}

}

int find_rnk(int l,int r,int k){

cnt=num=0;

for(int i=r;i;i-=lb(i)){

tem[++cnt]=rt[i];

}

for(int i=l-1;i;i-=lb(i)){

tmp[++num]=rt[i];

}

return query_rnk(1,len,k)+1;

}

int find_pri(int l,int r,int k){

int rk=find_rnk(l,r,k)-1;

if(rk==0) return 0;

return find_num(l,r,rk);

}

int find_nxt(int l,int r,int k){

if(k==len) return len+1;

int rk=find_rnk(l,r,k+1);

if(rk==r-l+2) return len+1;

return find_num(l,r,rk);

}

signed main(){

n=read();m=read();

tot=cnt=num=0;

for(int i=1;i<=n;i++){

a[i]=read();

lsh[++len]=a[i];

}

for(int i=1;i<=m;i++){

q[i].a=read();q[i].b=read();q[i].c=read();

if(q[i].a!=3) q[i].d=read();

else lsh[++len]=q[i].c;

if(q[i].a==4 || q[i].a==5) lsh[++len]=q[i].d;

}

sort(lsh+1,lsh+len+1);

len=unique(lsh+1,lsh+len+1)-lsh-1;

for(int i=1;i<=n;i++){

a[i]=lower_bound(lsh+1,lsh+1+len,a[i])-lsh;

add(i,1);

}

lsh[0]=-inf;

lsh[len+1]=inf;

for(int i=1;i<=m;i++){

if(q[i].a==3){

add(q[i].b,-1);

a[q[i].b]=lower_bound(lsh+1,lsh+1+len,q[i].c)-lsh;

add(q[i].b,1);

}

if(q[i].a==1){

q[i].d=lower_bound(lsh+1,lsh+1+len,q[i].d)-lsh;

printf("%d\n",find_rnk(q[i].b,q[i].c,q[i].d));

}

if(q[i].a==2){

printf("%d\n",lsh[find_num(q[i].b,q[i].c,q[i].d)]);

}

if(q[i].a==4){

q[i].d=lower_bound(lsh+1,lsh+1+len,q[i].d)-lsh;

printf("%d\n",lsh[find_pri(q[i].b,q[i].c,q[i].d)]);

}

if(q[i].a==5){

q[i].d=lower_bound(lsh+1,lsh+1+len,q[i].d)-lsh;

printf("%d\n",lsh[find_nxt(q[i].b,q[i].c,q[i].d)]);

}

}

return 0;

}

同样是对比(平衡树使用Treap):

此处权值树的常数优势就更为明显了。但相应的,权值树的空间劣势在逐渐暴露。有兴趣深入学习权值线段树树套树的同学可以参考这篇日报,在本文不作深入展开。

4 权值树的习题精选

(限于篇幅,此处习题不给出讲解)

杂题

P1600 天天爱跑步

P1972 [SDOI2009]HH的项链

P4113 [HEOI2012]采花

P2468 [SDOI2010]粟粟的书架

P3168 [CQOI2015]任务查询系统

P3157 [CQOI2011]动态逆序对

P3810 【模板】三维偏序(陌上花开)

扫描线

P1856 [USACO5.5]矩形周长Picture

P5490 【模板】扫描线

P1904 天际线

二维偏序

P2163 [SHOI2007]园丁的烦恼

P5816 [CQOI2010]内部白点

P3431 [POI2005]AUT-The Bus

P1502 窗口的星星

P4390 [BOI2007]Mokia 摩基亚

Copyright © 2088 世界杯女足_足球歌曲世界杯主题曲 - luxiuying.com All Rights Reserved.
友情链接