数据结构-线段树

目录

什么是线段树

线段树的建立

区间修改

区间查询


什么是线段树

线段树 (Segment Tree)是一种数据结构,它主要用于维护区间信息 (要求满足结合律),它可以实现 𝑂(log⁡𝑛) 的区间修改 ,还可以同时支持多种操作(加、乘)。

从数据结构的角度来说,线段树是用一个完全二叉树来存储对应于其每一个区间(segment)的数据。该二叉树的每一个结点中保存着相对应于这一个区间的信息。同时,线段树所使用的这个二叉树是用一个数组保存的,与堆(Heap)的实现方式相同。

例如,给定一个长度为N的数组arr,其所对应的线段树T各个结点的含义如下:

  1. T的根结点代表整个数组所在的区间对应的信息,及arr[0:N]不含N)所对应的信息。
  2. T的每一个叶结点存储对应于输入数组的每一个单个元素构成的区间arr[i]所对应的信息,此处0≤i<N
  3. T的每一个中间结点存储对应于输入数组某一区间arr[i:j]对应的信息,此处0≤i<j<N

以根结点为例,根结点代表arr[0:N]区间所对应的信息,接着根结点被分为两个子树,分别存储arr[0:(N-1)/2]arr[(N-1)/2+1:N]两个子区间对应的信息。也就是说,对于每一个结点,其左右子结点分别存储母结点区间拆分为两半之后各自区间的信息。也就是说对于长度为N的输入数组,线段树的高度为logN

对于一个线段树来说,其应该支持的两种操作为:

  1. Update:更新输入数组中的某一个元素并对线段树做相应的改变。
  2. Query:用来查询某一区间对应的信息(如最大值,最小值,区间和等)。

线段树的建立

如果有一个数组[1,2,3,4,5],那么它对应的线段树大概长这个样子:

每个节点 𝑝 的左右子节点的编号分别为 2p 和 2p+1 ,假如节点 p 储存区间 [l, r] 的和,设 mid=[(l+r)/2⌋ ,那么两个子节点分别储存 [𝑙, mid] 和 [mid+1,r] 的和。可以发现,左节点对应的区间长度,与右节点相同或者比之恰好多1。

如何从数组建立一棵线段树?我们可以考虑递归地进行。

cpp 复制代码
void build(st l = 1, st r = n, int p = 1)
{
    if (l == r) {// 到达叶子节点
        tree[p] = arr[l]; // 用数组中的数据赋值
    } else {
        st mid = (l + r) / 2;
        build(l, mid, p*2); // 左子节点
        build(mid + 1, r, p*2 + 1); //右子节点
        tree[p] = tree[p*2] + tree[p*2 + 1]; // 该节点的值等于左右子节点之和
    }
}

这里用一张gif展现上述的过程:


区间修改

在讲区间修改前,要先引入一个"懒标记 "的概念。懒标记是线段树的精髓所在。对于区间修改,朴素的想法是用递归 的方式一层层修改(类似于线段树的建立),但这样的时间复杂度比较高。使用懒标记后,对于那些正好是线段树节点的区间,我们不继续递归下去,而是打上一个标记 ,将来要用到它的子区间 的时候,再向下传递

cpp 复制代码
void update(st l, st r, int d, int p = 1, st cl = 1, st cr = n)
{
    if (cl > r || cr < l) // 区间无交集
        return; // 剪枝
    else if (cl >= l && cr <= r) // 当前节点对应的区间包含在目标区间中
    {
        tree[p] += (cr - cl + 1) * d; // 更新当前区间的值
        if (cr > cl) // 如果不是叶子节点
            mark[p] += d; // 给当前区间打上标记
    }
    else // 与目标区间有交集,但不包含于其中
    {
        st mid = (cl + cr) / 2;
        mark[p*2] += mark[p]; // 标记向下传递
        mark[p*2 + 1] += mark[p];
        tree[p*2] += mark[p] * (mid - cl + 1); // 往下更新一层
        tree[p*2 + 1] += mark[p] * (cr - mid);
        mark[p] = 0; // 清除标记
        update(l, r, d, p*2, cl, mid); // 递归地往下寻找
        update(l, r, d, p*2 + 1, mid + 1, cr);
        tree[p] = tree[p*2] + tree[p*2 + 1]; // 根据子节点更新当前节点的值
    }
}

更新时,我们是从最大的区间开始,递归向下处理。注意到,任何区间都是线段树上某些节点的并集。于是我们记目标区间为 [𝑙,𝑟] ,当前区间为 [𝑐𝑙,𝑐𝑟] , 当前节点为 𝑝 ,我们会遇到三种情况:

1. 当前区间与目标区间没有交集:这时直接结束递归。

2.当前区间被包括在目标区间里:

这时可以更新当前区间,别忘了乘上区间长度:

cpp 复制代码
tree[p] += (cr - cl + 1) * d;

然后打上懒标记(叶子节点可以不打标记,因为不会再向下传递了):这个标记表示"该区间上每一个点都要加上d"。因为原来可能存在标记,所以是+=而不是=。

cpp 复制代码
mark[p] += d;

3.当前区间与目标区间相交,但不包含于其中:

这时把当前区间一分为二,分别进行处理。如果存在懒标记,要先把懒标记传递给子节点(注意也是+=,因为原来可能存在懒标记):

cpp 复制代码
st mid = (cl + cr) / 2;
mark[p*2] += mark[p];
mark[p*2 + 1] += mark[p];

两个子节点的值也就需要相应的更新(后面乘的是区间长度):

cpp 复制代码
tree[p*2] += mark[p] * (mid - cl + 1);
tree[p*2 + 1] += mark[p] * (cr - mid);

不要忘记清除该节点的懒标记:

cpp 复制代码
mark[p] = 0;

这个过程并不是递归的,我们只往下传递一层,以后要用再才继续传递。其实我们常常把这个传递过程封装成一个函数:

cpp 复制代码
inline void push_down(st p, st len)
{
    //懒标记向下传递
    mark[p*2] += mark[p];
    mark[p*2 + 1] += mark[p];
    //对应线段数值改变
    tree[p*2] += mark[p] * (len - len / 2);
    tree[p*2 + 1] += mark[p] * (len / 2); // 右边的区间可能要短一点
    mark[p] = 0;//清除该节点的懒标记
}

然后在update函数中这样调用:

cpp 复制代码
push_down(p, cr - cl + 1);

传递完标记后,再递归地去处理左右两个子节点。

下面显示了区间 [1,4] 加上1的过程:


区间查询

有了区间修改的经验,区间查询的方法完全类似,一样的递归,一样自顶至底地寻找,一样的合并信息:

cpp 复制代码
ll query(st l, st r, int p = 1, st cl = 1, st cr = n)
{
    if (cl > r || cr < l)
        return 0;
    else if (cl >= l && cr <= r)
        return tree[p];
    else
    {
        st mid = (cl + cr) / 2;
        push_down(p, cr - cl + 1);
        return query(l, r, p*2, cl, mid) + query(l, r, p*2 + 1, mid + 1, cr); 
    }
}
相关推荐
龙的爹233312 分钟前
论文翻译 | The Capacity for Moral Self-Correction in Large Language Models
人工智能·深度学习·算法·机器学习·语言模型·自然语言处理·prompt
Back~~42 分钟前
MFC1(note)
学习
鸣弦artha44 分钟前
蓝桥杯——杨辉三角
java·算法·蓝桥杯·eclipse
我是聪明的懒大王懒洋洋1 小时前
力扣力扣力:动态规划入门(1)
算法·leetcode·动态规划
未知陨落1 小时前
数据结构——二叉搜索树
开发语言·数据结构·c++·二叉搜索树
丶Darling.1 小时前
Day44 | 动态规划 :状态机DP 买卖股票的最佳时机IV&&买卖股票的最佳时机III
算法·动态规划
engchina1 小时前
Oracle ADB 导入 BANK_GRAPH 的学习数据
数据库·学习·oracle·graph
一丝晨光2 小时前
gcc 1.c和g++ 1.c编译阶段有什么区别?如何知道g++编译默认会定义_GNU_SOURCE?
c语言·开发语言·c++·gnu·clang·gcc·g++
TN_stark9322 小时前
多进程/线程并发服务器
服务器·算法·php
Komorebi.py2 小时前
【Linux】-学习笔记03
linux·笔记·学习