数据结构学习笔记 - 线段树(基础)

本文详细介绍了线段树这一数据结构,包括其定义、特点、建树过程、单点修改、区间查询以及延迟标记的概念。线段树在处理区间修改和查询问题时具有高效性,通过延迟标记可以实现O(logN)的时间复杂度。此外,文中提供了一个完整的线段树模板代码示例,涵盖了区间修改、单点修改和区间查询三种基本操作。
摘要由CSDN通过智能技术生成

前言

 先来看个题目:洛谷 P3372 【模板】线段树 1

众所周知线段树这个数据结构,比较麻烦。但在解决类似于区间修改和查询这种问题的时候会很方便,其实也没有那么方便。具体来说,之所以不用暴力或者树状数组来进行上述操作,是因为有的题目对算法效率要求高,普通的算法就不行了。总结来说,我们在学习线段树之前,应该要理解我们为什么要学线段树,学了之后在哪种情况下会用到,和怎么把模板变成题目要求的代码。这些问题是我们要考虑到的。

定义

线段树是一种基于分治思想的二叉树结构,用于在区间上进行信息统计。与按照二进制(2的次幂)进行区间划分的树状数组相比,线段树是一种更加通用的结构。

线段树有如下的特点:

  1. 线段树的每个节点都代表一个区间。
  2. 线段树具有唯一的根节点,代表的区间是整个统计范围,如 [ 1 , N ]。
  3. 线段树的每个叶节点都代表一个长度位 1 的元区间 [ x , x ]。
  4. 对于每个内部节点 [ l , r ],它的左子节点是 [ l , mid ],右子节点是 [ mid + 1 , r ],其中           mid = ( l + r ) / 2 (向下取整)。

 

 可以发现,除去最后一层,整棵线段树一定是一棵完全二叉树,树的深度也就是著名的 O(log N)因此,我们可以按照与二叉树类似的“父子2倍”节点编号方法:

  1. 根节点编号为 1。
  2. 编号为 x 的节点的左子节点编号为 x * 2,右子节点编号为 x * 2 + 1。

由此我们就可以简单地使用一个 struct 来保存线段树。当然,树的最后一层节点在数组中保存的位置不是连续的,直接空出数组中多余的位置即可。在理想情况下,N个叶节点的满二叉树有 2N - 1 个节点。因为在上述存储方式下,最后还有一层产生了空余,所以保存线段树的数组长度要不小于 4N 才能保证不会越界。

建树

线段树的基本用途是对序列进行维护,支持查询与修改指令。给定一个长度为 N 的序列 A,我们可以在区间 [ 1 , N ] 上建立一棵线段树,每个叶节点 [ i , i ] 保存 A[i] 的值。线段树的二叉树结构可以很方便地从下往上传递信息。

inline void build(long long k,long long l,long long r){
    tree[k].l=l,tree[k].r=r;
    if(tree[k].l==tree[k].r){//从区间找到单点 
        scanf("%lld",&tree[k].w);//读入该位置的值 
        return ;
    }
    long long mid=(l+r)>>1;//类似于二分 
    build(k*2,l,mid);
    build(k*2+1,mid+1,r);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;//按题目要求加和 
}

单点修改

在线段树中,根节点(编号为 1 的节点)是执行各种指令的入口。我们需要从根节点出发,递归找到代表区间 [ x , x ] 的叶节点,然后从下往上更新 [ x , x ] 以及它的所有祖先节点上保存的信息,时间复杂度为 O(log N)。

inline void change_point(long long k,long long a,long long y){
	if(tree[k].l==tree[k].r){
		tree[k].w+=y;
		tree[k].f+=y;
		return;
	}
	if(tree[k].f) down(k);
	long long mid=(tree[k].l+tree[k].r)>>1;
	if(a<=mid) change_point(k*2,a,y);
	else change_point(k*2+1,a,y);
	tree[k].w=tree[k*2].w+tree[k*2+1].w;
}

区间查询

对于这个操作,我们只需要从根节点开始,递归执行以下过程:

  1. 若 [ l , r ] 完全覆盖了当前节点代表的区间,则立即回溯,并且返回该节点的 w 值。
  2. 若左子节点与 [ l , r ] 有重叠部分,则递归访问左子节点。
  3. 若右子节点与 [ l , r ] 有重叠部分,则递归访问右子节点。
inline void find(long long k,long long a,long long b){
    if(tree[k].l>=a&&tree[k].r<=b){
        ans+=tree[k].w;
        return ;
    }
    if(tree[k].f) down(k);
    long long mid=(tree[k].l+tree[k].r)/2;
    if(a<=mid) find(k*2,a,b);
    if(b>mid) find(k*2+1,a,b);
}

该查询过程会把询问区间 [ l , r ] 在线段树上分成 O(log N) 个节点,取它们的求和值作为答案。

延迟标记

在线段树的“区间查询”指令中,每当遇到被询问区间 [ l , r ] 完全覆盖的节点时,可以立即把该节点上存储的信息作为候选答案返回。我们已经知道,被询问区间 [ l , r ] 在线段树上会被分成 O(log N)个小区间(节点),从而在 O(log N) 的时间内求出答案。不过,在“区间修改”指令中,如果某个节点被修改区间 [ l , r ] 完全覆盖,那么以该节点为根的整棵子树中的所有节点存储的信息都会发生改变,若逐一更新,将使得一次区间修改指令的时间复杂度增加到 O(N)

其实,如果我们在一次修改指令中发现节点 p 代表的区间 [ pl , pr ] 被修改区间 [ l , r ] 完全覆盖,并且逐一更新了子树 p 中的所有节点,但是在之后的查询指令中却根本没有用到 [ l , r ] 的子区间作为候选答案,那么更新 p 的整棵子树是没有必要的

所以,我们可以在这种情况下,在节点 p 增加一个标记,代表“该节点曾经被修改,但其子节点尚未被更新”。当后续的指令中,需要从节点 p 向下递归,我们再检查 p 是否具有标记。若有标记,就根据标记信息更新 p 的两个子节点,同时为 p 的两个子节点增加标记,然后清除 p 的标记。

这样一来,每条查询或修改指令的时间复杂度都降低到了 O(log N)。这些标记被称为“延迟标记”。延迟标记提供了线段树中从上往下传递信息的方式,是很重要的解题思路。

标记下放例如:

inline void down(long long k){//标记下放 
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;//记得清0
}

例题代码示例:

#include<iostream>
#include<cmath>
#include<cstring>
#include<cstdio>
#include<algorithm>
using namespace std;

const int maxn=500001;
long long n,p,a,b,m,x,y,ans;

struct node{
	long long l,r,w,f;
}tree[4*maxn+1];

inline void build(long long k,long long l,long long r){
    tree[k].l=l,tree[k].r=r;
    if(tree[k].l==tree[k].r){//从区间找到单点 
        scanf("%lld",&tree[k].w);//读入该位置的值 
        return ;
    }
    long long mid=(l+r)/2;//类似于二分 
    build(k*2,l,mid);
    build(k*2+1,mid+1,r);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;//按题目要求加和 
}

inline void down(long long k){//标记下放 
    tree[k*2].f+=tree[k].f;
    tree[k*2+1].f+=tree[k].f;
    tree[k*2].w+=tree[k].f*(tree[k*2].r-tree[k*2].l+1);
    tree[k*2+1].w+=tree[k].f*(tree[k*2+1].r-tree[k*2+1].l+1);
    tree[k].f=0;//记得清0
}

inline void change_line(long long k,long long a,long long b,long long y){//区间修改 
    if(tree[k].l>=a&&tree[k].r<=b){
        tree[k].w+=(tree[k].r-tree[k].l+1)*(long long)y;//注意这里!
        tree[k].f+=y;//这里可以不再往下走,当需要的时候再标记下放 
        return ;
    }
    if(tree[k].f) down(k);
    long long mid=(tree[k].l+tree[k].r)>>1;
    if(a<=mid) change_line(k*2,a,b,y);
    if(b>mid) change_line(k*2+1,a,b,y);
    tree[k].w=tree[k*2].w+tree[k*2+1].w;
}

inline void change_point(long long k,long long a,long long y){
	if(tree[k].l==tree[k].r){
		tree[k].w+=y;
		tree[k].f+=y;
		return;
	}
	if(tree[k].f) down(k);
	long long mid=(tree[k].l+tree[k].r)>>1;
	if(a<=mid) change_point(k*2,a,y);
	else change_point(k*2+1,a,y);
	tree[k].w=tree[k*2].w+tree[k*2+1].w;
}

inline void find(long long k,long long a,long long b){
    if(tree[k].l>=a&&tree[k].r<=b){
        ans+=tree[k].w;
        return ;
    }
    if(tree[k].f) down(k);
    long long mid=(tree[k].l+tree[k].r)/2;
    if(a<=mid) find(k*2,a,b);
    if(b>mid) find(k*2+1,a,b);
}

int main(){
	scanf("%lld %lld",&n,&m);
	build(1,1,n);
	while(m--){
		scanf("%lld",&p);
		if(p==1){//区间修改 
			scanf("%lld %lld %lld",&a,&b,&y);
			change_line(1,a,b,y);
		}
		if(p==3){//单点修改 
			scanf("%lld %lld",&a,&y);
			change_point(1,a,y);
		}
		if(p==2){//区间查询 
			ans=0;
			scanf("%lld %lld",&a,&b);
			find(1,a,b);
			printf("%lld\n",ans);
		}
	}
	return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值