高级数据结构——线段树总结
本蒟蒻最近在做线段树的题,做了一小部分,有感而发,故写下这篇博客,如有错误,请大佬指出。
线段树,作为一种高级数据结构,而其作用与分块、树状数组均有一脉相承的部分,而且有的题均可以使用上面的两种算法去解决(当然只是一部分题)
对于线段树的介绍,我也就不再多说了,即对于数列将其放在树上进行维护一些值,在题目的要求下进行修改和更新,最后得到答案。
首先我们先上一两道模板题
1、洛谷P3372线段树1(加操作)
这是一道十分经典的线段树模板,当然他的解法也有其他方法,(十分重要)。
下面是代码:
#include<bits/stdc++.h> #define ll long long const int N=5e6+8; using namespace std; ll n,m,tag[N],ans[N],a[N]; ll ls(ll x){ return x<<1; }//左儿子扩展 (x*2) ll rs(ll x){ return x<<1|1; }//右儿子扩展(x*2+1) void up(ll x){ ans[x]=ans[ls(x)]+ans[rs(x)]; }// 求出当前点的总和值 void build(ll l,ll r,ll p){ if(l==r){ ans[p]=a[l]; return; } ll mid=(l+r)>>1; build(l,mid,ls(p));//左儿子建树 build(mid+1,r,rs(p));//右儿子建树 up(p);//进行整合 } void f(ll l,ll r,ll p,ll k){//k是懒(延迟)标记的数值 tag[p]+=k; ans[p]+=(r-l+1)*k; }//tag[]懒标记点。 void down(ll l,ll r,ll p){ ll mid=(l+r)>>1; f(l,mid,ls(p),tag[p]);//左传 f(mid+1,r,rs(p),tag[p]);//右传 tag[p]=0;//懒标记传递结束,清零 } void xg(ll xl,ll xr,ll l,ll r,ll p,ll k){//修改操作 if(l>=xl&&xr>=r){//当前区间被所查询的区间包含 ans[p]+=(r-l+1)*k; tag[p]+=k; return ; } down(l,r,p);//懒坐标传递 ll mid=(l+r)>>1; if(xl<=mid){ xg(xl,xr,l,mid,ls(p),k); } if(xr>mid){ xg(xl,xr,mid+1,r,rs(p),k); } up(p);//更新该节点 } ll query(ll gl,ll gr,ll l,ll r,ll p){ ll an=0; if(gl<=l&&gr>=r){ return ans[p]; } ll mid=(l+r)>>1; down(l,r,p); if(gl<=mid){ an+=query(gl,gr,l,mid,ls(p)); } if(gr>mid){ an+=query(gl,gr,mid+1,r,rs(p)); } return an; } ll fl,x,y,z; int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%lld",&a[i]); } build(1,n,1); for(int i=1;i<=m;i++){ scanf("%d",&fl); if(fl==1){ scanf("%lld%lld%lld",&x,&y,&z); xg(x,y,1,n,1,z); } if(fl==2){ scanf("%lld%lld",&x,&y); printf("%lld\n",query(x,y,1,n,1)); } } return 0; }
下一道模板题:
2、洛谷P3373线段树2(加乘操作)
这道题与上一道题同理,up和down的部分需要修改一下即可。(再给大家一道双倍经验题(P2023 [AHOI2009]维护序列)
下面就是代码:
#include<bits/stdc++.h> #define ll long long using namespace std; const int N=5e6+7; ll ans[N],a[N],at[N],ml[N]; ll fl,n,m,P,x,y,z; ll ls(ll x){ return x<<1; } ll rs(ll x){ return x<<1|1; } void up(ll p){ ans[p]=(ans[ls(p)]+ans[rs(p)])%P; } void build(ll l,ll r,ll p){ at[p]=0; ml[p]=1; if(l==r){ ans[p]=a[l]; return; } ll mid=(l+r)>>1; build(l,mid,ls(p)); build(mid+1,r,rs(p)); up(p); } void down(ll l,ll r,ll p){ ml[ls(p)]=(ml[ls(p)]*ml[p])%P; ml[rs(p)]=(ml[rs(p)]*ml[p])%P; at[ls(p)]=(at[ls(p)]*ml[p])%P; at[rs(p)]=(at[rs(p)]*ml[p])%P; ans[ls(p)]=(ans[ls(p)]*ml[p])%P; ans[rs(p)]=(ans[rs(p)]*ml[p])%P; ml[p]=1;//乘懒标记传递完成,清零处理,乘法优先于加法,所以先计算乘法再计算加法 ll mid=(l+r)>>1; at[ls(p)]=(at[ls(p)]+at[p])%P; at[rs(p)]=(at[rs(p)]+at[p])%P; ans[ls(p)]=(ans[ls(p)]+(mid-l+1)*at[p])%P; ans[rs(p)]=(ans[rs(p)]+(r-mid)*at[p])%P; at[p]=0; } void xg(ll gl,ll gr,ll l,ll r,ll p,ll k){ if(l>=gl&&gr>=r){ at[p]=(k+at[p])%P; ans[p]=((r-l+1)*k+ans[p])%P; return ; } down(l,r,p); ll mid=(l+r)>>1; if(gl<=mid){ xg(gl,gr,l,mid,ls(p),k); } if(gr>mid){ xg(gl,gr,mid+1,r,rs(p),k); } up(p); }//修改+; void ch(ll cl,ll cr,ll l,ll r,ll p,ll k){ if(l>=cl&&r<=cr){ ml[p]=(ml[p]*k)%P; at[p]=(at[p]*k)%P; ans[p]=(ans[p]*k)%P; return ; } down(l,r,p); ll mid=(l+r)>>1; if(cl<=mid){ ch(cl,cr,l,mid,ls(p),k); } if(cr>mid){ ch(cl,cr,mid+1,r,rs(p),k); } up(p); }//修改*; ll query(ll al,ll ar,ll l,ll r,ll p){ ll an=0; if(al<=l&&ar>=r){ return ans[p]%P; } down(l,r,p); ll mid=(l+r)>>1; if(al<=mid){ an=an+query(al,ar,l,mid,ls(p)); } if(ar>mid){ an=an+query(al,ar,mid+1,r,rs(p)); } return an%P; } int main(){ scanf("%lld%lld",&n,/*&m,*/&P); for(int i=1;i<=n;i++) scanf("%lld",&a[i]); sacnf("%lld",&m); build(1,n,1); for(int i=1;i<=m;i++){ scanf("%lld",&fl); if(fl==1){ scanf("%lld%lld%lld",&x,&y,&z); ch(x,y,1,n,1,z); } if(fl==2){ scanf("%lld%lld%lld",&x,&y,&z); xg(x,y,1,n,1,z); } if(fl==3){ scanf("%lld%lld",&x,&y); printf("%lld\n",(query(x,y,1,n,1))%P); } } return 0; }
当你写完这两三道模板题之后那么恭喜你,你已经线段树入门了。
下面,就是对于线段树的进一步的运用。
使用线段树维护方差,平均数等操作
3、P1471 方差
这道题的题面我就不再进行粘贴,具体的意思大概如下:
给定一个数列,其中有三个操作,
1、将L到R的区间内的数加上k。
2、查询L到R区间的平均数。
3、查询L到R区间的方差。
我表示这道题第一开始看的时候对于操作1,2基本就是十分木板的操作,但是当我看到第三个操作时,我有点蒙。
但是,将方差的公式进行展开,就可以得到一个十分明晰的思路
\[
\begin{aligned} \sum_{i=1}^{n}\left(x_{i}-\overline{x}\right)^{2} &=\sum_{i=1}^{n}\left(x_{i}^{2}-2 x_{i} \overline{x}+\overline{x}^{2}\right) \\ &=\sum_{i=1}^{n} x_{i}^{2}+n \overline{x}^{2}-2 \overline{x} \sum_{i=1}^{n} x_{i} \\ &=\sum_{i=1}^{n} x_{i}^{2}+n \overline{x}^{2}-2 \overline{x} \cdot n \overline{x} \\ &=\sum_{i=1}^{n} x_{i}^{2}-\overline{n} \overline{x}^{2} \end{aligned}
\]
最后我们再将这个公式除上n得到最终的式子,这样我们就有了初步的思路,维护这个序列的两个值一个是序列和,一个是序列的平方和。这样我们就可以得到我们所求的答案。
现在我们来考虑如何使用线段树来维护平方和。
我们首先将平方和的公式展开
\[
(x+k)^{2}=x^{2}+2 k x+k^{2}
\]
我们可以知道这样就可以维护平方和:
代码:
#include <bits/stdc++.h> using namespace std; const int N=1e6+10; inline int read(){ int x=0,f=1; char ch=getchar(); while(ch<'0'||ch>'9'){ if(ch=='-'){ f=-1; } ch=getchar(); } while(ch>='0'&&ch<='9'){ x=(x<<1)+(x<<3)+(ch^48); ch=getchar(); } return x*f; } struct node{ int l,r,siz; double ave,sum,tag; }t[N]; int n,m; double a[N]; inline int ls(int x){return x<<1;} inline int rs(int x){return x<<1|1;} inline void upd(int x){ t[x].ave=t[ls(x)].ave+t[rs(x)].ave; t[x].sum=(t[ls(x)].sum+t[rs(x)].sum); } inline void build(int l,int r,int x){ // cout<<1; t[x].l=l,t[x].r=r,t[x].siz=r-l+1; if(l==r){ t[x].ave=a[l]; t[x].sum=a[l]*a[l]; return ; } int mid=(l+r)>>1; build(l,mid,ls(x)); build(mid+1,r,rs(x)); upd(x); } inline void pushdown(int x){ if(!t[x].tag) return; t[ls(x)].tag+=t[x].tag; t[rs(x)].tag+=t[x].tag; t[ls(x)].sum+=(2.0*t[ls(x)].ave*t[x].tag+t[ls(x)].siz*t[x].tag*t[x].tag);//维护平方和 t[rs(x)].sum+=(2.0*t[rs(x)].ave*t[x].tag+t[rs(x)].siz*t[x].tag*t[x].tag); t[ls(x)].ave+=t[x].tag*t[ls(x)].siz; t[rs(x)].ave+=t[x].tag*t[rs(x)].siz; t[x].tag=0; return ; } inline void add(int l,int r,int x,double k){ if(t[x].l>=l&&t[x].r<=r){ t[x].sum+=(2.0*t[x].ave*k+t[x].siz*k*k); t[x].ave+=k*t[x].siz; t[x].tag+=k; return ; } pushdown(x); int mid=(t[x].l+t[x].r)>>1; if(l<=mid){ add(l,r,ls(x),k); } if(r>mid){ add(l,r,rs(x),k); } upd(x); } inline double query(int l,int r,int x,bool flag){ if(t[x].l>=l&&t[x].r<=r){ if(flag==1){ return t[x].ave;} else return t[x].sum; } pushdown(x); int mid=(t[x].l+t[x].r)>>1; double res=0; if(l<=mid){ res+=query(l,r,ls(x),flag); } if(r>mid){ res+=query(l,r,rs(x),flag); } return res; } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%lf",&a[i]); } build(1,n,1); while(m--){ int op,l,r; double k; double ans1,ans2; op=read(); if(op==1){ scanf("%d%d%lf",&l,&r,&k); add(l,r,1,k); } if(op==2){ scanf("%d%d",&l,&r); ans1=query(l,r,1,1); printf("%.4lf",(double)ans1/(r-l+1)); puts(""); } if(op==3){ scanf("%d%d",&l,&r); ans1=query(l,r,1,1); ans2=query(l,r,1,0); printf("%.4lf",(double)ans2/(r-l+1)-ans1/(r-l+1)*ans1/(r-l+1)); puts(""); } } return 0; }
值得注意的是这道题的题目要求是实数范围内,所以千万千万不要忘掉使用,
double(手动滑稽)
这就是线段树去维护不同值的一个十分好的题,也十分的考验思维。
下面,我们看下一题,一道维护0/1序列的题。
因为时间原因,所以就先写到这里,表示一定不会咕掉的(心虚),明天再更。