线段树(Segment Tree)
1.建树
首先我们要明白线段树中的每个节点都代表一个区间,而对于线段树中的每个内部节点 \(\left[l,r\right]\),它的左子节点是 \(\left[l,mid\right]\),右子节点是 \(\left[mid + 1,r\right]\),其中 \(mid = (l+r)/2\)(向下取整)。
然后我们可以让根节点的编号为 \(1\),而编号为 \(x\) 的节点的左子节点编号为 \(x*2\),右子节点编号为 \(x*2+1\)。这样的话,就能用结构体来存储线段树了,但要注意存储线段树的数组长度要不小于 \(4N\),其中 \(N\) 为节点个数。
线段树基本就是对序列进行维护,以区间求和为例,代码如下:
struct Tree{
int l,r,sum;
}tree[SIZE*4];
void build(int k,int l,int r){
tree[k].l=l,tree[k].r=r;
if(l==r){
tree[k].sum=a[l];
return;
}
int mid=(l+r)/1;
build(k*2,l,mid);
build(k*2+1,mid+1,r);
tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;//从下往上传递信息
}
build(1,1,n);//调用
2. 单点修改
单点修改是将 \(A_x\) 的值修改为 \(v\)。
我们需要从根节点开始,递归寻找到代表区间 \(\left[x,x\right]\) 的叶子节点,然后从下往上更新它以及它的所有祖先节点上保存的信息,时间复杂度为 \(\mathcal{O}(log N)\)。
void change(int k,int x,int v){
if(tree[k].l==tree[k].r){
tree[k].sum=v;
return;
}
int mid=(tree[k].l+tree[k].r)/2;
if(x<=mid)change(k*2,x,v);
else change(k*2+1,x,v);
tree[k].sum=tree[k*2].sum+tree[k*2+1].sum;
}
change(1,x,v);//调用
3. 区间查询
这里以区间求和为例,查询一个序列 \(A\) 的区间 \(\left[l,r\right]\) 的和,即 \(\sum\limits_{i=l}^r A_i\)。
我们只需要从树根开始递归即可。
int query(int k,int l,int r){
if(tree[k].l>=l&&tree[k].r<=r){
return tree[k].sum;
}
int ans=0,mid=(tree[k].l+tree[k].r)/2;
if(mid>=l)ans+=query(k*2,l,r);
if(mid<r)ans+=query(k*2+1,l,r);
return ans;
}
4. 延迟标记
我们在执行修改指令时,在回溯之前向此节点 \(p\) 增加一个标记,标识此节点被修改但子节点未更新。在后续指令中,如果要从 \(p\) 向下递归,就再检查它是否具有标记,如果有,则根据标记更新子节点,并为子节点增加标记,删除 \(p\) 的标记。
总的来说,就是对任意节点的修改操作都延迟到“后续操作中递归进入其父节点时”再执行,这里以 Luogu P2023 为例。
#include<bits/stdc++.h>
#define int long long
#define MAX 100005
using namespace std;
int n,p,m,a[MAX],op,t,g,c;
struct MRS{
int l,r,sum,add,mul;
}tree[MAX*4];
void build(int k,int l,int r){
tree[k].l=l,tree[k].r=r,tree[k].mul=1;
if(l==r){
tree[k].sum=a[l]%p;
return;
}
int mid=(l+r)>>1;
build(k*2,l,mid);
build(k*2+1,mid+1,r);
tree[k].sum=(tree[k*2].sum+tree[k*2+1].sum)%p;
}
void pushdown(int k){
// if(tree[k].add){
tree[k*2].sum=(tree[k].mul*tree[k*2].sum+(tree[k].add*(tree[k*2].r-tree[k*2].l+1))%p)%p;
tree[k*2+1].sum=(tree[k].mul*tree[k*2+1].sum+(tree[k].add*(tree[k*2+1].r-tree[k*2+1].l+1))%p)%p;
tree[k*2].mul=(tree[k].mul*tree[k*2].mul)%p;
tree[k*2+1].mul=(tree[k].mul*tree[k*2+1].mul)%p;
tree[k*2].add=(tree[k].mul*tree[k*2].add+tree[k].add)%p;
tree[k*2+1].add=(tree[k].mul*tree[k*2+1].add+tree[k].add)%p;
tree[k].add=0;
tree[k].mul=1;
// }
}
void ADD(int k,int l,int r,int d){
if(tree[k].l>=l&&tree[k].r<=r){
tree[k].sum+=d*(tree[k].r-tree[k].l+1);
tree[k].sum%=p;
tree[k].add=(tree[k].add+d)%p;
return;
}
pushdown(k);
tree[k].sum=(tree[k*2].sum+tree[k*2+1].sum)%p;
int mid=(tree[k].l+tree[k].r)>>1;
if(l<=mid)ADD(k*2,l,r,d);
if(r>mid)ADD(k*2+1,l,r,d);
tree[k].sum=(tree[k*2].sum+tree[k*2+1].sum)%p;
}
void MUL(int k,int l,int r,int d){
if(tree[k].l>=l&&tree[k].r<=r){
tree[k].sum=(tree[k].sum*d)%p;
tree[k].add=(tree[k].add*d)%p;
tree[k].mul=(tree[k].mul*d)%p;
return;
}
pushdown(k);
tree[k].sum=(tree[k*2].sum+tree[k*2+1].sum)%p;
int mid=(tree[k].l+tree[k].r)>>1;
if(l<=mid)MUL(k*2,l,r,d);
if(r>mid)MUL(k*2+1,l,r,d);
tree[k].sum=(tree[k*2].sum+tree[k*2+1].sum)%p;
}
int query(int k,int l,int r){
if(tree[k].l>=l&&tree[k].r<=r){
return tree[k].sum;
}
pushdown(k);
int mid=(tree[k].l+tree[k].r)>>1,ans=0;
if(l<=mid)ans+=query(k*2,l,r),ans%=p;
if(r>mid)ans+=query(k*2+1,l,r),ans%=p;
return ans;
}
signed main(){
ios::sync_with_stdio(false);
cin.tie(0);
cout.tie(0);
cin>>n>>p;
for(int i=1;i<=n;i++){
cin>>a[i];
}
cin>>m;
build(1,1,n);
for(int i=1;i<=m;i++){
cin>>op;
if(op==1){
cin>>t>>g>>c;
MUL(1,t,g,c);
}else if(op==2){
cin>>t>>g>>c;
ADD(1,t,g,c);
}else if(op==3){
cin>>t>>g;
cout<<query(1,t,g)<<'\n';
}
}
return 0;
}
总而言之,线段树在 \(\mathcal O(log n)\) 的时间内解决区间修改与查询问题中及其有用,值得学习。
标签:int,线段,tree,笔记,节点,学习,add,mul,sum From: https://www.cnblogs.com/MithrilSwordXIV/p/17968669