树状数组 线段树

Posted ppxppx

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树状数组 线段树相关的知识,希望对你有一定的参考价值。

树状数组

树状数组的基本用途是维护序列的前缀和,相比前缀和数组,树状数组优势在于高效率的单点修改,单点增加(前缀和数组单点修改效率比较低)

因为树状数组的思想,原理还是很好理解的,就直接讲基本算法;

1 lowbit函数

关于lowbit这个函数,可能会有点难以理解,但其实你不理解也没关系,把模板背下来就好

根据任意正整数关于2的不重复次幂的唯一分解性质,例如十进制21用二进制表示为10101,其中等于1的位是第0,2,4(最右端是第0位)位,即21被二进制分解成(2^4+2^2+2^0);

进一步地,整个区间[1,21]可以分成如下3个小区间:

长度为(2^4)的小区间[1,(2^4)];

长度为(2^2)的小区间[(2^4+1),(2^4+2^2)];

长度为(2^0)的小区间[(2^4+2^2+1),(2^4+2^2+2^0)];

对于给定的初始序列A,我们可以建立一个数组c,c[x]表示序列A的区间[x-lowbit(x)+1,x)]中所有数的和;

int lowbit(int x){return x&-x;}

2 单点增加操作

void update(int x,int y){
    for(;x<=n;x+=lowbit(x))
        c[x]+=y;
}

3 查询前缀和

int sum(int x){
    int ans=0;
    for(;x;x-=lowbit(x)) ans+=c[x];
    return ans;
}

4 扩展

上述查询前缀和是统计[1,x]的前缀和,若要统计区间[x,y]的和,则调用sum函数即可:sum(y)-sum(x-1);

多维树状数组:

(扩充为m维)将原来的修改和查询函数中的一个循环,改成m个循环m维数组c中的操作;

(n*m)的二维数组为例:

将(x,y)的值加上z,不是把区间[x,y]中的每个值加z
int update(int x,int y,int z){
    int i=x;
    while(i<=n){
        int j=y;
        while(j<=m){
            c[i][j]+=z;
            j+=lowbit(j);
        }
        i+=lowbit(i);
    }
}

int sum(int x,int y){
    int res=0,i=x;
    while(i>0){
        int j=y;
        while(j>0){
            res+=c[i][j];
            j-=lowbit(j);
        }
        i-=lowbit(i);
    }
    return res;
}

注意树状数组的下标绝对不能为0,因为lowbit(0)=0,这样会陷入死循环

两道模板题,多打打模板~~

https://www.luogu.org/problemnew/show/P3374
https://www.luogu.org/problemnew/show/P3368

线段树

线段树是一种基于分治思想的二叉树结构,比树状数组更加通用,下文总结会比较这两种数据结构;

线段树的基本用途是对序列进行维护,支持查询与修改指令;

线段树的每个节点都代表一个区间;

线段树具有唯一的根节点,代表的是整个区间,即[1,n];

线段树的每个叶节点都代表一个长度为1的区间,即[x,x];

对于每个内部节点[l,r],它的左子节点是[l,mid],右子节点是[mid+1,r],其中mid=(l+r)/2(向下取整);(子节点与父节点的性质近似于二叉堆的结构);

**保存的数组长度要不小于4*n;**

1 建树:线段树的二叉树结构可以很方便地从下往上传递信息;

下面以区间最大值为例:

struct TREE{
    int l,r;
    int dat;
}t[n*4];
//一般会用结构体存储线段树

void build(int p,int l,int r){
    t[p].l=l;t[p].r=r;//节点p代表区间[l,r]
    if(l==r){t[p].dat=a[l];return;}//叶节点
    int mid=(l+r)/2;
    build(p*2,l,mid);//左子节点
    build(p*2+1,mid+1,r);//右子节点
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
    //从下往上传递信息
}

build(1,1,n);//调用入口

2 单点修改:线段树中,根节点(编号为1的节点)是执行各种指令的入口;上述建树过程中的调用入口的第一个1就是这个道理;

我们还是以区间最大值问题为例

void change(int p,int x,int v){
    if(t[p].l==t[p].r){t[p].dat=v;return;}
    //找到叶节点
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid) change(p*2,x,v);
    else change(p*2+1,x,v);
    //判断x属于哪边区间
    t[p].dat=max(t[p*2].dat,t[p*2+1].dat);
}

change(1,x,v);

3 区间查询

以查询区间最大值为例:

若[l,r]完全覆盖了当前节点代表的区间,则立即回溯,并且该节点的dat值为候选答案

若左子节点与[l,r]有重叠部分,则递归访问左子节点;

若右子节点与[l,r]有重叠部分,则递归访问右子节点;

(这里自己稍微理解一下就懂了,或者自己画个图)

假设我们查询区间[2,7]的最大值,现在有一个节点的代表区间为[3,5],那么属于完全覆盖的情况,return,并且该节点的dat值为候选答案;

现在还有[2,3]和[5,7]这两个区间没有处理,我们继续寻找节点,如果有一个节点与该区间有交集,即有重叠的区间,我们就继续向下访问该节点,直至找到一个节点完全被[2,3]或[5,7]覆盖,那么跟上面一样处理即可;(记住我们是从上往下访问线段树,所以区间范围从上往下是越来越小的)

int ask(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r)return t[p].dat;
    int mid=(t[p].l+t[p].r)/2;
    int val=-(1<<30);
    if(l<=mid) val=max(val,ask(p*2,l,r));
    if(r>mid) val=max(val,ask(p*2+1,l,r));
    return val;
}

ask(1,l,r);

延迟标记:标识该节点曾经被修改,但其子节点尚未被修改(即一个节点被打上延迟标记的同时,它自身保存的信息应该已经被修改完毕)

通俗地讲,我们在进行区间增加操作时,如果去更改区间中的每个数(即遍历到每个叶节点),时间复杂度会增加到O(N);

试想一下,有可能我们修改了该区间,但所有的查询中与该区间没有关系(即该区间没有对我们的答案产生贡献),相当于我们做了一次无用的操作;

于是就可以用到延迟标记,我们在执行修改指令时,给节点p一个标记,标识该节点曾经被修改,但其子节点尚未被修改,如果在后续的查询指令中,我们需要知道p节点的子节点的信息,如果p节点有标记,那么更新p的两个子节点,同时给p的两个子节点打上延迟标记,然后清除p的标记;

以区间增加问题为例:

void spread(int p){
    if(t[p].add){//如果节点P有标记
 t[p*2].sum+=t[p].add*(t[p*2].r-t[p*2].l+1);
    //更新左子节点信息
t[p*2+1].sum+=t[p].add*(t[p*2+1].r-t[p*2+1].l+1);
    t[p*2].add+=t[p].add;//给左子节点打延迟标记
    t[p*2+1].add+=t[p].add;
    t[p].add=0;//清除p的标记
    }
}

void change(int p,int x,int y,int v){
    if(x<=t[p].l&&y>=t[p].r){//完全覆盖
        t[p].sum+=v*(t[p].r-t[p].l+1);
        //更新节点信息
        t[p].add+=v;//给节点打上延迟标记
        return;
    }
    spread(p);    //下传延迟标记
    int mid=(t[p].l+t[p].r)/2;
    if(x<=mid) change(p*2,x,y,v);
    if(y>mid) change(p*2+1,x,y,v);
    t[p].sum=t[p*2].sum+t[p*2+1].sum;
}

long long ask(int p,int l,int r){
    if(l<=t[p].l&&r>=t[p].r) 
        return t[p].sum;
    spread(p);//下传延迟标记
    int mid=(t[p].l+t[p].r)/2;
    long long ans=0;
    if(l<=mid) ans+=ask(p*2,l,r);
    if(r>mid) ans+=ask(p*2+1,l,r);
    return ans;
}

最后比较一下树状数组和线段树:

树状数组可以实现单点修改和区间求和(如果将序列进行差分,那么还可以实现区间价值和单点询问);

线段树能实现树状数组的所有操作,除此之外,还可以通过标记下传或标记永久化进行区间修改和区间询问;

但树状数组的常数比线段树小,实现也较为简单(线段树真的是随随便便100行,看来我还是太蒻了)

总之,能用树状数组就用树状数组吧,毕竟一般都只能用线段树;


以上是关于树状数组 线段树的主要内容,如果未能解决你的问题,请参考以下文章

吊打线段树的超级树状数组

线段树/树状数组问题 | 问题集合

线段树与树状数组的对比应用

P3372 模板线段树 1(区间修改区间查询)(树状数组)

线段树&树状数组

Leetcode-线段树和树状数组