[数据结构]线段树
线段树
线段树是一种平衡二叉树,其核心思想为二分+分治+懒标记,可在 O ( log 2 n ) O(\log_2n) O(log2n)的复杂度内完成单点修改、区间修改、区间查询等操作。
设原序列长度为 n n n,则每个节点 i d ( 1 ≤ i d ≤ 4 × n ) id(1\le id \le 4\times n) id(1≤id≤4×n)都有一段原序列的管理区间 [ L , R ] [L,R] [L,R]:
-
若 L = R L=R L=R,则其为叶子节点,其管理区间长度为 1 1 1,存储即为原序列数据。 t r e e [ i d ] = a [ l ] tree[id]=a[l] tree[id]=a[l];
-
若 L < R L<R L<R,则为非叶子节点,其管理区间内存储了该区间内的某种信息(如区间最值、区间GCD、区间和等)。每个节点 i d id id有两个孩子,左孩子为 2 × i d 2\times id 2×id,管理区间为 [ L , M ] [L,M] [L,M];右孩子为 2 × i d + 1 2\times id+1 2×id+1,管理区间为 [ M + 1 , R ] [M+1,R] [M+1,R], M = L + R 2 M=\frac{L+R}{2} M=2L+R。 t r e e [ i d ] = o p t ( t r e e [ 2 × i d ] , t r e e [ 2 × i d + 1 ] ) tree[id]=opt(tree[2\times id],tree[2\times id+1]) tree[id]=opt(tree[2×id],tree[2×id+1])。
线段树适用条件:大区间的值,可由若干个小区间的值合并而来。如可重复贡献性问题、区间之和、区间之积等。
int n,v[n];
struct node{
long long data,tag=0;//管理区间内的数据,懒标记标签
}tree[n<<2];//线段树数组开4*n
int opt(int a,int b);//具体操作函数
建树
- 向下递归:先左后右,向下递归到叶子节点,向叶子节点中传入原数组值
- 向上传递:叶子节点被传入数据后,需组织成区间信息,并向其父节点传递(向上传递)
void pushup(int id){//向上传递
tree[id].data=opt(tree[id<<1].data,tree[id<<1|1].data);
}
void build(int id,int l,int r){//id:当前递归节点 [l,r]:当前递归节点管理区间
if(l==r) tree[id].data=a[l];//已递归到叶子节点
else{
int mid=l+r>>1;
build(id<<1,l,mid);//先递归左孩子2*id,管理区间[l,mid]
build(id<<1|1,mid+1,r);//然后递归到右孩子2*id+1,管理区间[mid+1,r]
pushup(id);//向上传递
}
}
//调用:build(1,1,n);
单点修改
设将 p o s pos pos处修改为 v a l u e value value:
- 向下递归:递归至叶子节点找pos。若 p o s ≤ m i d pos\le mid pos≤mid,则进入左孩子管理区间继续递归;若 p o s > m i d pos>mid pos>mid,则进入右孩子管理区间继续递归
- 向上传递:修改后向上传递信息
void change(int id,int l,int r,int pos,int value){//当前递归节点及其管理区间
if(l==r) tree[id].data=value,a[pos].data=value;//递归至叶子节点修改信息
else{
int mid=l+r>>1;
if(pos<=mid) change(id<<1,l,mid);//若pos在[l,mid],进入左孩子管理区间
else chage(id<<1|1,mid+1,r);//若pos在[mid+1,r],进入右孩子管理区间
pushup(id);
}
}
区间修改
关键操作:懒标记(Lazy Tag)。对每个节点定义区间标记 t a g tag tag,用于记录其管理区间 [ L , R ] [L,R] [L,R]内的统一修改,而对其中每个元素先不进行修改。仅当该管理区间元素的修改一致性被破坏时,才将自身 t a g tag tag向下传递给左右孩子,并清空自身 t a g tag tag。懒标记的作用就是等待下次向下传递时递归给左右孩子。
算法流程:设修改区间 [ m l , m r ] [ml,mr] [ml,mr],当前递归区间 [ l , r ] [l,r] [l,r]:
- 若 [ l , r ] ⊂ [ m l , m r ] [l,r]\subset[ml,mr] [l,r]⊂[ml,mr](完全被修改区间覆盖),则直接将 [ l , r ] [l,r] [l,r]打标记
- 否则无法被修改区间完全覆盖,说明当前递归区间存在不应被修改的元素。向下对其左右子树传递区间标记,并分别递归其左右子树。
void addtag(int id,int l,int r,int d){//为节点打标记,此处以+d为例
tree[id]+=d,tree[id]+=d*(r-l+1);
}
void pushdown(int id,int l,int r){//向下对左右孩子传递标记
if(tree[id].tag){
int mid=l+r>>1;
addtag(id<<1,l,mid,tree[id].tag);
addtag(id<<1|1,mid+1,r,tree[id].tag);
tree[id].tag=0;
}
}
void modify(int id,int l,int r,int ml,int mr,int d){//以区间[ml,mr]均+d为例
if(ml<=l&&r<=mr){
addtag(id,l,r,d);
}else{
pushdown(id,l,r);
int mid=l+r>>1;
if(ml<=mid) modify(id<<1,l,mid,ml,mr,d);
if(mr>mid) modify(id<<1|1,mid+1,r,ml,mr,d);
pushup(id);
}
}
区间查询
设查询区间 [ q l , q r ] [ql,qr] [ql,qr],当前递归节点 i d id id管理区间为 [ l , r ] [l,r] [l,r]:
- 若
[
l
,
r
]
⊂
[
q
l
,
q
r
]
[l,r]\subset[ql,qr]
[l,r]⊂[ql,qr]:直接返回
tree[id].data
- 若 [ q l , q r ] [ql,qr] [ql,qr]与 [ l , r ] [l,r] [l,r]部分重叠,则下传懒标记,并分别递归其左右孩子
int query(int id,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return tree[id].data;
pushdown(id,l,r);
int mid=l+r>>1,ans ;//ans=0(区间之和) 1(区间之积) MAX/MAX+1(MAX/MIN)
if(ql<=mid) ans=opt(ans,query(id<<1,l,mid,ql,qr));
if(qr>mid) ans=opt(ans,query(id<<1|1,mid+1,r,ql,qr));
return ans;
}
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
2023-12-28 学习心得:函数指针和函数指针数组