线段树
线段树学习笔记
1. 线段树简介
线段树,是一种二叉搜索树,其每一个节点表示了一段区间。
线段树支持的操作有:
- 区间求 和 或 最大/最小值,时间复杂度
(p.s.后面代码均以求和为例)
有同学说:“这还不简单?我用前缀和 O(1) 就能做,要他干什么?”
不要着急,因为线段树还有这个功能:
- 单点修改,时间复杂度
前面那位同学:“我不服,我用树状数组,也是 O(logN)!!!”
看我接着放大招,线段树还可以这样玩:
- 区间修改,时间复杂度还是
某同学:“……”
是的,线段树就是一个如此强大的数据结构,让我们接着一起欣赏 她 它的魅力
2.线段树时空复杂度分析
-
空间复杂度
线段树在形态上属于平衡二叉树(此平衡树非彼平衡树 光速逃 ),也就是砍掉最后一层之后是满二叉树,就像 二叉堆 一样
由于线段树肥肠整齐,我们可以用数组来存
每个节点 u
的左右儿子分别为 u*2
和 u*2+1
(在写代码时一般写成位运算的形式,及u<<1
u<<1|1
)
除最后一层外,节点总个数是
所以总共的节点数最多为
因此,线段树的数组要开到
这里我们可以写一个简易的程序验证一下:
#include<iostream>
using namespace std;
int a,b;
void build(int u,int l,int r)
{
a++;
b=max(b,u);
if(l==r)
return;
int m=l+r>>1;
build(u<<1,l,m);
build(u<<1|1,m+1,r);
}
int main()
{
int n;
scanf("%d",&n);
build(1,1,n);
printf("length of the array: %d, number of nodes: %d, size of the space: %d;\n",n,a,b);
return 0;
}
当
eg:
输入:
1048580
输出:
length of the array: 1048580, number of nodes: 2097159, size of the space: 3670017;
-
时间复杂度
显而易见,一棵
最坏情况下,每次更新或查询时,都要从根节点递归到最后一层,因此时间复杂度为
当然,除了两条贯穿上下的链之外,还会有一些长度为
3.线段树的存储
有两种方法
第一种就直接开一个简单的数组,但这样每个函数都需要把当前节点表示的区间的左右两个端点传成参数,太麻烦了,参数太多也不太好看
第二种方法看着就舒服多了。
需要一个结构体
struct Tree{
int l,r;//线段所覆盖区间的左右端点
int v;//存储的信息,即sum,max或min;
int tag;//懒标记
}tr[4*N];
4.线段树5大函数
-
build
顾名思义,新建一颗线段树树
递归建树
void build_tree(int u,int l,int r)
{
if(l==r)//叶节点
{
tr[u]={l,r,w[l],0};
return;
}
tr[u]={l,r};
int m=l+r>>1;
build_tree(u<<1,l,m);//递归建左子树
build_tree(u<<1|1,m+1,r);//右子树
pushup(u);
}
-
pushup
根据两个子节点信息求出父节点信息(可以解决单点修改)
void pushup(int u)
{
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
-
pushdown
把父节点记录的信息下发到子结点上(区间修改必备操作)
过程中需要用到懒标记
懒标记的基本原理:在反复的操作过程中,同一个节点可能被修改很多次,不如把每一次修改都先记录下来,攒到一起,最后只修改一次(非常妙的思路hh)
void pushdown(int u)
{
if(tr[u].tag)
{
tr[u<<1].tag+=tr[u].tag;
tr[u<<1|1].tag+=tr[u].tag;//下放懒标记
tr[u<<1].sum+=(ll)tr[u].tag*(tr[u<<1].r-tr[u<<1].l+1);
tr[u<<1|1].sum+=(ll)tr[u].tag*(tr[u<<1|1].r-tr[u<<1|1].l+1);//更新区间和
tr[u].tag=0;//标记清空
}
}
-
update
更新/修改
将某个区间修改为某个数/加上某个数(代码以加上为例)
void update(int u,int l,int r,int k)
{
if(tr[u].l>=l&&tr[u].r<=r)//被完全包含在内
{
tr[u].sum+=(ll)k*(tr[u].r-tr[u].l+1);
// if(tr[u].r>tr[u].l)//叶节点可以不用记,因为不会再往下传导
tr[u].tag+=k;
return;
}
pushdown(u);//更新
int m=tr[u].l+tr[u].r>>1;
if(l<=m)//和左面有交集
update(u<<1,l,r,k);
if(r>m)//和右面有交集
update(u<<1|1,l,r,k);
pushup(u);
}
-
query
查询某个区间的和/最大值/最小值(代码以和为例)
ll query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r)
return tr[u].sum;
pushdown(u);//更新
ll res=0;
int m=tr[u].l+tr[u].r>>1;
if(l<=m)//和左面有交集
res+=query(u<<1,l,r);
if(r>m)//和右面有交集
res+=query(u<<1|1,l,r);
return res;//返回结果^_^
}
5.完整代码
这个代码可以轻松水过洛谷模板题 - P3372 【模板】线段树 1
#include<iostream>
using namespace std;
typedef long long ll;
const int N=100010;
struct Tree{
int l,r;//区间左右端点
ll sum;//区间和,用不用long long 看具体情况
ll tag;//懒标记 ZZZ
}tr[4*N];//空间开4倍,不开见祖宗(逃~)
int n,q;//n为数组大小,q是操作个数
int w[N];//原数组
void pushup(int u)
{
tr[u].sum=tr[u<<1].sum+tr[u<<1|1].sum;
}
void pushdown(int u)
{
if(tr[u].tag)
{
tr[u<<1].tag+=tr[u].tag;
tr[u<<1|1].tag+=tr[u].tag;//下放懒标记
tr[u<<1].sum+=(ll)tr[u].tag*(tr[u<<1].r-tr[u<<1].l+1);
tr[u<<1|1].sum+=(ll)tr[u].tag*(tr[u<<1|1].r-tr[u<<1|1].l+1);//更新区间和
tr[u].tag=0;//标记清空
}
}
void build_tree(int u,int l,int r)
{
if(l==r)//叶节点
{
tr[u]={l,r,w[l],0};
return;
}
tr[u]={l,r};
int m=l+r>>1;
build_tree(u<<1,l,m);//递归建左子树
build_tree(u<<1|1,m+1,r);//右子树
pushup(u);
}
void update(int u,int l,int r,int k)
{
if(tr[u].l>=l&&tr[u].r<=r)//被完全包含在内
{
tr[u].sum+=(ll)k*(tr[u].r-tr[u].l+1);
// if(tr[u].r>tr[u].l)//叶节点可以不用记,因为不会再往下传导
tr[u].tag+=k;
return;
}
pushdown(u);//更新
int m=tr[u].l+tr[u].r>>1;
if(l<=m)//和左面有交集
update(u<<1,l,r,k);
if(r>m)//和右面有交集
update(u<<1|1,l,r,k);
pushup(u);
}
ll query(int u,int l,int r)
{
if(tr[u].l>=l&&tr[u].r<=r)
return tr[u].sum;
pushdown(u);//更新
ll res=0;
int m=tr[u].l+tr[u].r>>1;
if(l<=m)//和左面有交集
res+=query(u<<1,l,r);
if(r>m)//和右面有交集
res+=query(u<<1|1,l,r);
return res;//返回结果^_^
}
int main()
{
cin>>n>>q;
for(int i=1;i<=n;i++)
cin>>w[i];//输入原数组
build_tree(1,1,n);//建立线段树
while(q--)
{
int opt,l,r,k;
cin>>opt;//操作种类
if(opt==1)
{
cin>>l>>r>>k;
update(1,l,r,k);//更新
}else{
cin>>l>>r;
cout<<query(1,l,r)<<endl;//查询
}
}
return 0;
}
恭喜你!线段树基本知识已经掌握辣~乌拉~~
小题一道试试水(简单的模板题):- P1253 [yLOI2018] 扶苏的问题
本文章到此结束,喜欢的童鞋点个赞呗 ~ 笔芯
追风赶月莫停留,平芜尽处是春山~
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」