前言
在对这三个数据结构进行了粗浅的学习之后,博主发现数据结构的世界是多么的美妙。
然后在博主的专业作死技能加持之下,三个数据结构的大战一触即发……
一、单点修改及区间查询
树状数组
对于某些题目(如,单点更新并且查询区间和),那么这个时候,我们会发现用树状数组也十分吃香,毕竟这正是树状数组所擅长的!它的时间复杂度均为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[i−1];
那么显然,我们可以得到这样的式子:a[i]=∑ij=1s[j]
则我们会发现
我们再展开一下,就会变成这样,一个三角形!
补全为矩形,然后减去补上的三角形,继续化简:
将后面的的∑ni=1(i−1)s[i]作为si进行维护,即si[i]=(i−1)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线段树:常数小,性质多,跑的也的确比普通线段树要快得多,但似乎应用上还有一些局限性,不一定适用于所有的题目。感觉很强的样子,只不过要搞懂它的精髓可能还需要时间……
普通线段树:很灵活,能够应对复杂多变的题目,到底是经典。