特别浅的浅谈线段树
Segment_Tree
线段树好题大赏
定义
线段树是一种二叉搜索树,线段树的每个结点都存储了一个区间,也可以理解成一个线段。
用处
维护区间信息。线段树可以在 的时间复杂度内实现单点修改,区间修改,区间查询等操作。
最典型的,也是最简单的就是 区间加 和 区间求和。
以 此题 为例,就代表最简单的线段树操作了。
树的储存
线段树比较直观的理解方式是看图:
(比较懒,直接从网上找了一个,我也不知道来自哪个 dalao 的博客)
这个图中每个节点有三个数:上边是点的编号,左边是区间的左端点,右边是区间的右端点。
个人习惯用结构体:
struct Seg_Tree{ int le;//区间左端点 int ri;//区间右端点 int val;//区间维护的值,图中未展示 int tag;//懒标记,之后会提到 }T[inf*4];
虽然图中的线段树不是每个节点都画了出来,但那些节点确确实实存在,因此线段树大概需要所维护的数组大小的 4 倍左右。
建树
对于线段树来说,每个节点 的左儿子的编号都是 ,右儿子的编号都是 ,同时区间大小对半分(见上图)。
通俗的理解一下,1 号节点代表整个区间,2 号节点代表左半个区间,3 号节点代表右半个区间。
这个代表可以是区间最值,区间和,区间 GCD 等题目要求维护的东西。
那以此题来说,就是区间和了。
建树的时候,先递归到叶子,将原数组的数分别赋到对应的叶节点,然后回溯时再将左右两个子树的权值加回来。
代码实现:
void build(int i,int l,int r) { T[i].le=l;T[i].ri=r; if(l==r) { T[i].val=a[l]; return; } int mid=(l+r)>>1; build(i*2,l,mid); build(i*2+1,mid+1,r); T[i].val=T[i*2].val+T[i*2+1].val; }
区间求和
基本思路就是若覆盖则返回,否则递归到下一层合适的区间再返回。
举个例子,还是由上图,手模一下查询区间 的和。
首先进入 1 号节点。
1 号节点太大, 无法将其覆盖,那么就找一号节点的两个子节点:2 号节点和 3 号节点。
2 号节点与 有交集,3 号节点与 并无交集,就递归到 2 号节点。同理,再递归到 4 号节点和 5 号节点。
这时注意 5 号节点。5 号节点不仅与 有交集,而且能被其覆盖,那么这时就可以直接返回 5 号节点的权值,并在回溯时累加到答案中。
Code:
int ask(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) return T[i].val; int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask(i<<1,l,r); if(mid<r)ans+=ask(i<<1|1,l,r); return ans; }
部分初学者可能会在递归传参的时候将区间写错(别问我怎么知道的),记住这句话:查询的区间不能变。
区间加
基本的思路和区间求和相似,同样是找交集。
但是每次都将所覆盖的区间全部更新的话复杂度是 ,而暴力的时间复杂度是 的,多个 。
如果想要我们的线段树比暴力快的话,我们需要引入一个 懒惰标记 ,顾名思义,就是偷懒。此时的单次修改时间复杂度就成了 。
因为每次修改不一定会马上被查询。比如上图中我更新 ,查询 ,那么这次的更新对下次的询问没有任何影响。我们就用懒标将这次更新储存下来,来表示 这个区间的每个元素都有 tag
还没加上 。
然后查询或者再次更新的时候,就将这个懒标下放到两个儿子上。
代码:
void update(int i,int l,int r,int k) { if(l<=T[i].le&&T[i].ri<=r) { T[i].val+=k*(T[i].ri-T[i].le+1); T[i].tag+=k; return; } if(T[i].tag)pushdown(i); int mid=(T[i].l+T[i].r)>>1; if(l<=mid)update(i<<1,l,r,k); if(mid<r)update(i<<1|1,l,r,k); T[i].val=T[i<<1].val+T[i<<1|1].val; }
pushdown
即为:
void pushdown(int i) { T[i<<1].val+=T[i].tag*(T[i<<1].ri-T[i<<1].le+1); T[i<<1|1].val+=T[i].tag*(T[i<<1|1].ri-T[i<<1|1].le+1); T[i<<1].tag+=T[i].tag; T[i<<1|1].tag+=T[i].tag; T[i].tag=0; }
那么区间求和也需要加上这个 pushdown
:
int ask(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) return T[i].val; if(T[i].tag)pushdown(i); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask(i<<1,l,r); if(mid<r)ans+=ask(i<<1|1,l,r); return ans; }
这里也有一个易混淆的点:懒标表示的是当前区间已经维护但其子区间仍未维护。注意区分。
完整代码
由于不是同一历史时期写的,可能码风会有不同,等有空了在维护吧。
const int inf=1e5+7; int n,m,a[inf]; struct Seg_Tree{ int le,ri; int val,tag; }T[inf<<2]; void build(int i,int l,int r) { T[i].le=l;T[i].ri=r; if(l==r) { T[i].val=a[l]; return; } int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); T[i].val=T[i<<1].val+T[i<<1|1].val; } void pushdown(int i) { T[i<<1].val+=(T[i<<1].ri-T[i<<1].le+1)*T[i].tag; T[i<<1|1].val+=(T[i<<1|1].ri-T[i<<1|1].le+1)*T[i].tag; T[i<<1].tag+=T[i].tag; T[i<<1|1].tag+=T[i].tag; T[i].tag=0; } void update(int i,int l,int r,int k) { if(l<=T[i].le&&T[i].ri<=r) { T[i].val+=(T[i].ri-T[i].le+1)*k; T[i].tag+=k; return; } if(T[i].tag)pushdown(i); int mid=(T[i].le+T[i].ri)>>1; if(l<=mid)update(i<<1,l,r,k); if(mid<r)update(i<<1|1,l,r,k); T[i].val=T[i<<1].val+T[i<<1|1].val; } int ask(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) return T[i].val; if(T[i].tag)pushdown(i); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask(i<<1,l,r); if(mid<r)ans+=ask(i<<1|1,l,r); return ans; } signed main() { n=re();m=re(); for(int i=1;i<=n;i++) a[i]=re(); build(1,1,n); for(int i=1;i<=m;i++) { int op=re(),l=re(),r=re(); if(op==1)update(1,l,r,re()); else wr(ask(1,l,r)),putchar('\n'); } return 0; }
习题
除了区间求和,动态区间 RMQ 问题也经常用线段树求解。
因为求和与 RMQ 都具有区间可加性,即知道两个子区间的和/最值就可以直接求出整个区间的和/最值。
不过对于静态区间 RMQ 问题,一个更快(指常数更小)的算法是 ST 表。
Code
const int inf=1e5+7; int n,m,a[inf]; struct Seg_Tree{ int le,ri,minn; }T[inf<<2]; void build(int i,int l,int r) { T[i].le=l,T[i].ri=r; if(l==r) { T[i].minn=a[l]; return; } int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); T[i].minn=min(T[i<<1].minn,T[i<<1|1].minn); } int ask(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) return T[i].minn; int mid=(T[i].le+T[i].ri)>>1,ans=2147483647; if(l<=mid)ans=min(ans,ask(i<<1,l,r)); if(mid<r)ans=min(ans,ask(i<<1|1,l,r)); return ans; } int main() { n=re();m=re(); for(int i=1;i<=n;i++) a[i]=re(); build(1,1,n); for(int i=1;i<=m;i++) { int l=re(),r=re(); wr(ask(1,l,r)),putchar(' '); } return 0; }
区间取反操作,只是在 tag
上有些小动作,其他部分基本没有什么区别。
用 tag
表示这个区间有没有被翻转,显然翻转两次的区间和未翻转的区间相同,那么每次异或 1 就可以了。
Code
const int inf=2e5+7; int n,m,a[inf]; struct Seg_Tree{ int le,ri,siz; int val,tag; }T[inf<<2]; void build(int i,int l,int r) { T[i].le=l,T[i].ri=r; T[i].siz=r-l+1; if(l==r) { T[i].val=a[l]; return; } int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); T[i].val=T[i<<1].val+T[i<<1|1].val; } void pushdown(int i) { T[i<<1].val=T[i<<1].siz-T[i<<1].val; T[i<<1|1].val=T[i<<1|1].siz-T[i<<1|1].val; T[i<<1].tag^=1,T[i<<1|1].tag^=1; T[i].tag=0; } void update(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) { T[i].val=T[i].siz-T[i].val; T[i].tag^=1; return; } if(T[i].tag)pushdown(i); int mid=(T[i].le+T[i].ri)>>1; if(l<=mid)update(i<<1,l,r); if(mid<r)update(i<<1|1,l,r); T[i].val=T[i<<1].val+T[i<<1|1].val; } int ask(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) return T[i].val; if(T[i].tag)pushdown(i); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask(i<<1,l,r); if(mid<r)ans+=ask(i<<1|1,l,r); return ans; } int main() { n=re();m=re(); for(int i=1;i<=n;i++) scanf("%1d",&a[i]); build(1,1,n); for(int i=1;i<=m;i++) { int op=re(),l=re(),r=re(); if(op)wr(ask(1,l,r)),putchar('\n'); else update(1,l,r); } return 0; }
状压线段树,建议先了解部分位运算知识。
通过观察可以发现,颜色数并不多,可以用一个 int
存下来。那么区间的颜色数就是两个子区间的颜色数之并集,就是两个数取或即可。
Code
const int inf=1e5+7; int n,m,k; struct Seg_Tree{ int le,ri; int val,tag; }T[inf<<2]; void build(int i,int l,int r) { T[i].le=l,T[i].ri=r; T[i].val=2; if(l==r)return; int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); } void pushdown(int i) { T[i<<1].val=T[i<<1|1].val=1<<T[i].tag; T[i<<1].tag=T[i<<1|1].tag=T[i].tag; T[i].tag=0; } void assign(int i,int l,int r,int k) { if(l<=T[i].le&&T[i].ri<=r) { T[i].val=1<<k; T[i].tag=k; return; } if(T[i].tag)pushdown(i); int mid=(T[i].le+T[i].ri)>>1; if(l<=mid)assign(i<<1,l,r,k); if(mid<r)assign(i<<1|1,l,r,k); T[i].val=T[i<<1].val|T[i<<1|1].val; } int ask(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) return T[i].val; if(T[i].tag)pushdown(i); int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans|=ask(i<<1,l,r); if(mid<r)ans|=ask(i<<1|1,l,r); return ans; } int main() { n=re();k=re();m=re(); build(1,1,n); for(int i=1;i<=m;i++) { char op[10]="";scanf("%s",op); int l=re(),r=re(); if(l>r)l^=r^=l^=r; if(op[0]=='C')assign(1,l,r,re()); else { int ls=ask(1,l,r),ans=0; while(ls) { if(ls&1)ans++; ls>>=1; } wr(ans),putchar('\n'); } } return 0; }
小清新线段树,就是在线段树上加剪枝。
可以发现,就算是 ,在开 次平方之后也变成了 。
而且 ,那么如果这个区间已经全是 了,就可以不操作,否则暴力单点修改整个区间。
const int inf=1e5+7; int n,m,a[inf]; struct Seg_Tree{ int le,ri; int val,siz; }T[inf<<2]; void build(int i,int l,int r) { T[i].le=l,T[i].ri=r; T[i].siz=r-l+1; if(l==r) { T[i].val=a[l]; return; } int mid=(l+r)>>1; build(i<<1,l,mid); build(i<<1|1,mid+1,r); T[i].val=T[i<<1].val+T[i<<1|1].val; } void update(int i,int l,int r) { if(T[i].le==T[i].ri) { T[i].val=sqrt(T[i].val); return; } if(T[i].val==T[i].siz)return; int mid=(T[i].le+T[i].ri)>>1; if(l<=mid)update(i<<1,l,r); if(mid<r)update(i<<1|1,l,r); T[i].val=T[i<<1].val+T[i<<1|1].val; } int ask(int i,int l,int r) { if(l<=T[i].le&&T[i].ri<=r) return T[i].val; int mid=(T[i].le+T[i].ri)>>1,ans=0; if(l<=mid)ans+=ask(i<<1,l,r); if(mid<r)ans+=ask(i<<1|1,l,r); return ans; } signed main() { n=re(); for(int i=1;i<=n;i++) a[i]=re(); build(1,1,n); m=re(); for(int i=1;i<=m;i++) { int op=re(),l=re(),r=re(); if(l>r)l^=r^=l^=r; if(op)wr(ask(1,l,r)),putchar('\n'); else update(1,l,r); } return 0; }
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· .NET10 - 预览版1新功能体验(一)