学习笔记——线段树

线段树(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)\) 的时间内解决区间修改与查询问题中及其有用,值得学习。

posted @ 2024-01-17 21:27  GyrthCurunír  阅读(2)  评论(0编辑  收藏  举报