线段树&&树状数组 总结

前言
在对这三个数据结构进行了粗浅的学习之后,博主发现数据结构的世界是多么的美妙。
然后在博主的专业作死技能加持之下,三个数据结构的大战一触即发……


一、单点修改及区间查询

树状数组

对于某些题目(如,单点更新并且查询区间和),那么这个时候,我们会发现用树状数组也十分吃香,毕竟这正是树状数组所擅长的!它的时间复杂度均为O(log(n)),相比于线段树,它的空间复杂度尤其小得多。

void update(int target[],int pos,int val)
{
    for(;pos<=n;pos+=lowbit(pos))
      target[pos]+=val;
}
int query(int target[],int pos)
{
    int sum=0;
    for(;pos;pos-=lowbit(pow))
      sum+=target[pos];
    return sum;
}

zkw线段树

然后接着,zkw线段树(%%%%)对此也是再擅长不过了,看起来十分的欢快。
毕竟是非递归的,常数小,在大部分情况下,可以大大加快操作速度。
思想
单点修改:先通过N直接找到叶子节点,然后再向上更新父亲节点。
区间查询:为了避免错误,我们在找叶子节点之前,先转闭区间为开区间获取指针。左边的指针如果是父亲的左子树就累加其右子树的答案,右边的指针如果是父亲的右子树就累加左子树的答案。直到两指针的父亲节点相同。
为了更好的理解,建议手动模拟~

void update(int pos,int val)
{
    for(pos+=N;pos;pos>>=1)
      sum[pos]+=val;
}
void query(int l,int r)
{
    int ans=0;
    for(l+=N-1,r+=N+1;l^r^1;l>>=1,r>>=1)
    {
        if(~l&1)
          ans+=sum[l^1];
        if(r&1)
          ans+=sum[r^1];
    }
}

普通线段树

但是同时,我们也知道,线段树更新、查询等操作的复杂度也为O(log(n))。但是线段树的常数较大。尤其对于这些比较入门的题目的时候,代码量会比较大,也就使得我们更容易出错。尽管如此,我们还是贴一下它操作的代码,以示敬意。

inline void pushup(int rt){sum[rt]=sum[rt<<1]+sum[rt<<1|1];}
inline void pushdown(int ln,int rn,int rt)
{
    lazy[rt<<1]+=lazy[rt];
    lazy[rt<<1|1]+=lazy[rt];
    sum[rt<<1]+=ln*lazy[rt];
    sum[rt<<1|1]+=rn*lazy[rt];
    lazy[rt]=0;
}
void update(int l,int r,int L,int c,int rt)
{
    if(l==r)
    {   
        sum[rt]+=c;  
        return;  
    }  
    int m=(l+r)>>1;  
    if(L<=m)
      update(l,m,L,c,rt<<1);  
    else
      update(m+1,r,L,c,rt<<1|1);  
    pushup(rt);   
}   
int query(int l,int r,int L,int R,int rt)
{
    if(L<=l&&r<=R)
      return sum[rt];
    int m=(l+r)>>1,ans=0;
    if(lazy[rt])
      pushdown(m-l+1,r-m,rt);
    if(L<=m)
      ans+=query(l,m,L,R,rt<<1);
    if(m<R)
      ans+=query(m+1,r,L,R,rt<<1|1);
    return ans;
}

由此可见,在应对一些简单问题的时候,线段树不一定是最好的选择。

二、区间更新及区间查询

树状数组

由于其在第一部分,时间及空间复杂度的优异表现,于是各路神犇就对其作出了更强的改进。
dalao says:乱写能AC,暴力踩标程
思想
以下的update(),query()函数的含义同上。
假设原数组为a,利用差分的思想处理出s数组,即s[i]=a[i]a[i1]
那么显然,我们可以得到这样的式子:a[i]=ij=1s[j]
则我们会发现

i=1na[i]=i=1nj=1is[j]

我们再展开一下,就会变成这样,一个三角形!
一张丑陋的截图
补全为矩形,然后减去补上的三角形,继续化简:
i=1na[i]=ni=1ns[i]i=1n(i1)s[i]

将后面的的ni=1(i1)s[i]作为si进行维护,即si[i]=(i1)s[i]
然后就可以做到区间更新与修改了!
scanf("%d",&type);  
if(type==1)//[l,r]+v  
{  
    scanf("%d%d%d",&l,&r,&v);
    update(s,l,v);
    update(s,r+1,-v);
    update(si,l,v*(a-1));
    update(si,r+1,-v*b);
}  
else//[l,r]->sum
{  
    scanf("%d%d",&l,&r);  
    sum1=(l-1)*query(s,l-1)-sigma(si,l-1);  
    sum2=r*query(s,r)-query(si,r);  
    printf("%lld\n",sum2-sum1);  
}  

zkw线段树

代码中一些变量的含义:
ln:s一路走来已经包含了几个数
rn:t一路走来已经包含了几个数
x:本层中包含的数
思想
lazy数组依然是懒惰标记,不过利用了差分的思想,化绝对为相对。因此,在更新与查询的时候,[L,R]所对应的区间上升到父亲相同之后,还要继续向上一直上升到根节点。

void update(int L,int R,int c){  
    int s,t,ln=0,rn=0,x=1;  
    for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1)
    {  
        sum[s]+=r*ln;  
        sum[t]+=c*rn;  
        if(~s&1) lazy[s^1]+=c,sum[s^1]+=c*x,ln+=x;  
        if( t&1) lazy[t^1]+=c,sum[t^1]+=c*x,rn+=x;  
    }  
    for(;s;s>>=1,t>>=1){  
        sum[s]+=c*ln;  
        sum[t]+=c*rn;  
    }   
}
int query(int L,int R){  
    int s,t,ln=0,rn=0,x=1;  
    int ans=0;  
    for(s=N+L-1,t=N+R+1;s^t^1;s>>=1,t>>=1,x<<=1){  
        if(lazy[s]) ans+=lazy[s]*ln;  
        if(lazy[t]) ans+=lazy[t]*rn;  
        if(~s&1) ans+=sum[s^1],ln+=x;  
        if( t&1) ans+=sum[t^1],rn+=x;   
    }  
    for(;s;s>>=1,t>>=1){  
        ans+=lazy[s]*ln;  
        ans+=lazy[t]*rn;  
    }  
    return ans;  
}  

普通线段树

相对来说,普通线段树应对的比较从容,代码量也没有增加太多,但依旧是最大的……

inline void pushup(int rt){sum[rt]=sum[rt<<1]+sum[rt<<1|1];}
inline void pushdown(int ln,int rn,int rt)
{
    lazy[rt<<1]+=lazy[rt];
    lazy[rt<<1|1]+=lazy[rt];
    sum[rt<<1]+=ln*lazy[rt];
    sum[rt<<1|1]+=rn*lazy[rt];
    lazy[rt]=0;
}
void update(int l,int r,int L,int R,int c,int rt)
{
    if(L<=l&&r<=R)
    {
        lazy[rt]+=c;
        sum[rt]+=(l-r+1)*c;
        return ;
    }
    int m=(l+r)>>1;
    if(L<=m)
      update(l,m,L,R,c,rt<<1);
    if(m<R)
      update(m+1,r,L,R,c,rt<<1|1);
    pushup(rt);
}
int query(int l,int r,int L,int R,int rt)
{
    if(L<=l&&r<=R)
      return sum[rt];
    int m=(l+r)>>1,ans=0;
    if(lazy[rt])
      pushdown(m-l+1,r-m,rt);
    if(L<=m)
      ans+=query(l,m,L,R,rt<<1);
    if(m<R)
      ans+=query(m+1,r,L,R,rt<<1|1);
    return ans;
}

由此可见,普通线段树相对于树状数组与zkw线段树来说,更加灵活多变,甚至能够应对更加复杂的情况而游刃有余。

三、总结

树状数组:如果能用的话,在时间、空间复杂度上估计都可以暴踩线段树(用了都说好)
zkw线段树:常数小,性质多,跑的也的确比普通线段树要快得多,但似乎应用上还有一些局限性,不一定适用于所有的题目。感觉很强的样子,只不过要搞懂它的精髓可能还需要时间……
普通线段树:很灵活,能够应对复杂多变的题目,到底是经典。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值