线段树详解(附图解,模板及经典例题分析)

1.基本概念

​ 线段树是一种常用来维护 区间信息 \color{Maroon}区间信息 区间信息 的数据结构

​ 可以在 O(logn) 的时间复杂度内实现单点修改、区间修改、区间查询(区间求和,求区间的最大值,求区间的最小值)等操作

​ 如下图所示,线段树是建立在区间基础上的树,树的每个节点都代表着一段区间【L,R】

在这里插入图片描述

对于每个段区间【L,R】来说,我们注意到

(1)若 L = R,说明这个结点只有一个点,那么它就是一个叶子节点

(2)若 L < R,说明这个节点代表的不止一个点,此时它就会有两个儿子

  • 左儿子:区间【L,M】
  • 右儿子:区间【M + 1,R】

其中 M = (L + R)/ 2

在实现时,我们考虑 递归建树 \color{CadetBlue}递归建树 递归建树 。即,设当前根结点为 u

  • 如果根结点管辖的区间长度已经为 1,则可以直接根据原数组a上相应位置的值初始化该节点
  • 否则将该区间从中点处分割为两个子区间【L,M】,【M + 1,R】,并分别进入左右节点的递归建树,最后将两个子节点的信息pushup到父节点u
struct Node// 线段树结构
{
    int l, r;
    int sum;// 【l, r】的区间和
    
}tr[4 * N];
void pushup(int u)// 将信息往上传递,在这里维护的是区间和
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
    
}
void build(int u, int l, int r)
{
    if(l == r) tr[u] = {l, r, a[r]};
    else
    {
        tr[u] = {l, r}; // 注意此行,若不初始化,会TLE
       	int mid = l + r >>1;
        build(u << 1, l, mid);// 左儿子
        build(u << 1 | 1, mid + 1, r);// 右儿子
        pushup(u);// 将信息传递给父节点
    }
    
}

思考:为什么对于线段树的数组空间我们要开到4倍的 N呢?

​ 如果采用堆式存储( 2u 是 u 的左儿子, 2u + 1 是 u 的右儿子),若有 n 个叶子节点,则 tr 数组的范围最大为 2 ⌈ l o g n ⌉ + 1 2^{\lceil{logn}\rceil + 1} 2logn+1

​ 容易知道线段树的深度是 ⌈ l o g n ⌉ \lceil{logn}\rceil logn 的,则在堆式情况下叶子节点(包括没有用到的叶子节点)数量为 2 ⌈ l o g n ⌉ 2^{\lceil{logn}\rceil} 2logn

​ 我们可以发现 2 ⌈ l o g n ⌉ + 1 n \frac{2^{\lceil{logn}\rceil + 1}}{n} n2logn+1 最大值在 n = 2 x + 1 ( x ∈ N + ) n = 2^x + 1 (x ∈ N_+) n=2x+1(xN+) 时取到,此时节点数为

2 ⌈ l o g n ⌉ + 1 − 1 = 2 x + 2 − 1 = 4 n − 5 2^{\lceil{logn}\rceil + 1 } - 1 = 2^{x + 2} - 1 = 4n - 5 2logn+11=2x+21=4n5 ,故我们可以直接开 4n 的 tr数组,减少计算时间

2.原理推导

( 1 ) 区间查询 − − O ( l o g n ) \color{Turquoise}(1) 区间查询 - - O(logn) (1)区间查询O(logn)

1. 若这个线段树区间被完全包括在目标区间里面,则直接返回这个区间的值 \color{skyBlue}1.若这个线段树区间被完全包括在目标区间里面,则直接返回这个区间的值 1.若这个线段树区间被完全包括在目标区间里面,则直接返回这个区间的值

在这里插入图片描述

if(tr[i].l >= l && tr[r].r <= r) // 线段树区间被完全包含
return tr[u].sum;

2. 若这个线段树区间的左儿子与目标区间有交集,那么搜索左儿子 \color{skyBlue}2.若这个线段树区间的左儿子与目标区间有交集,那么搜索左儿子 2.若这个线段树区间的左儿子与目标区间有交集,那么搜索左儿子

在这里插入图片描述

int sum = 0;
if(l <= mid) sum = query(u << 1, l, r); // 与左儿子有交集

3. 若这个线段树区间的右儿子与目标区间有交集,那么搜索右儿子 \color{skyBlue}3.若这个线段树区间的右儿子与目标区间有交集,那么搜索右儿子 3.若这个线段树区间的右儿子与目标区间有交集,那么搜索右儿子

在这里插入图片描述

int sum = 0;
// if(l <= mid) sum = query(u << 1, l, r); 与左儿子有交集
if( r > mid) sum += query(u << 1 | 1, l, r); // 与右儿子有交集

例如,如下图所示,如果要查询的区间为 【1,5】的和,那么可以直接获取Tr[1] 的值(15)即可

在这里插入图片描述

如果要查询的区间为【3,5】,此时就不能直接获取区间的值,但是【3,5】可以拆分成【3,3】【4,5】,可以通过合并这两个区间的答案来求得这个区间的和

一般地,如果要查询的区间为【l,r】,则可以将其拆成至多为 O(logn)极大的区间,合并这些区间即可求出区间【l,r】的答案

Code

int query(int u, int l, int r)// u 为父节点,区间【l,r】为目标区间
{
    if(tr[u].l >= l && tr[u].r <= r) 
        return tr[u].sum;// 如果线段树区间被完全包括在目标区间里面
    
    int sum = 0;
    int mid = tr[u].l + tr[u].r >> 1;
    
    // 左儿子与目标区间有交集
    if(l <= mid) sum = query(u << 1, l, r);// 因为sum初始化为 0,所以不写 += 也可以
    //右儿子与目标区间有交集
    if(r > mid) sum += query(u << 1 | 1, l, r);
    
    return sum;
}

( 2 ) 单点修改 − − O ( l o g n ) \color{Turquoise}(2) 单点修改- - O(logn) (2)单点修改O(logn)

​ 单点修改,换句话说就是如何实现点更新,假使我们要将 原数组 a[3] + 7 ,则更新后的线段树应该变成

在这里插入图片描述

​ 也就是说,当我们更新原数组a[]某一个位置 x 的数值时,在线段树中包含此值的区间都需要被更新,那么有多少个节点需要更新呢,也就是最大要达到多深才完成全部区间的更新呢?根据二叉树的性质,容易知道线段树的深度是 ⌈ l o g n ⌉ \lceil{logn}\rceil logn的,故此每次更新的时间复杂度为O(logn)

​ 对于每次的更新,我们发现,无论你更新的是哪一个节点,最终都会将信息pushup到根结点,而把这个往上推的过程逆过来就是 从根结点开始,找到左子树或者右子树中包含需要更新的叶子节点,自顶向下更新即可 \color{Orange}从根结点开始,找到左子树或者右子树中包含需要更新的叶子节点,自顶向下更新即可 从根结点开始,找到左子树或者右子树中包含需要更新的叶子节点,自顶向下更新即可,所以我们还是采用递归的方法实现线段树的点更新

Code

void modify(int u, int x, int v) // 根结点,插入的位置,插入的值
{
    if(tr[u].l == tr[u].r) tr[u].sum += v;
    else
    {
        int mid = tr[u].l + tr[u].r >> 1;
        if(x <= mid) modify(u << 1, x, v);// 左子树包含需要更新的叶子节点
        else modify(u << 1 | 1, x, v); // 右子树包含需要更新的叶子节点
        
        pushup(u);
    }
}

3.初步总结

目前为止,我们就把最基本的线段树操作过了一遍,必须抓住 四个核心函数 \color{Red}四个核心函数 四个核心函数

  • p u s h u p    ( i n t   u ) \color{Green}pushup~~\color{CadetBlue}(int ~u) pushup  (int u) :用子节点信息更新当前父节点信息
  • b u i l d    ( i n t   u ,   i n t   l ,   i n t   r ) \color{Maroon}build ~~\color{CadetBlue}(int ~u,~int ~ l, ~ int~r) build  (int u, int l, int r) : 在一段区间上,初始化线段树
  • m o d i f y   ( i n t   u ,   i n t   x ,   i n t   v ) \color{purple}modify~\color{CadetBlue}(int ~u,~int ~ x, ~ int~v) modify (int u, int x, int v): 修改目标位置 x 的属性
  • q u e r y    ( i n t   u ,   i n t   l ,   i n t   r ) \color{Turquoise}query~~\color{CadetBlue}(int ~u,~int ~ l, ~ int~r) query  (int u, int l, int r): 查询区间【L, R】的属性(max,区间和,min等等)

细节 : 线段树的下标从 1 开始

4.线段树拓展及其例题


以上内容尚未完全,随着今后学习的推进,我会继续对其进行补充与完善。

【预告】

  1. 线段树拓展
  2. 经典例题
  • 16
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 7
    评论
下面是一个简单的线段的 Python 实现模板: ```python class SegmentTree: def __init__(self, arr): self.arr = arr self.tree = [0] * (4 * len(arr)) self.build(0, 0, len(arr) - 1) def build(self, node, start, end): if start == end: self.tree[node] = self.arr[start] else: mid = (start + end) // 2 left_child = 2 * node + 1 right_child = 2 * node + 2 self.build(left_child, start, mid) self.build(right_child, mid + 1, end) self.tree[node] = self.tree[left_child] + self.tree[right_child] def query(self, node, start, end, left, right): # 区间完全包含在查询区间内 if left <= start and right >= end: return self.tree[node] # 区间完全不在查询区间内 if end < left or start > right: return 0 mid = (start + end) // 2 left_child = 2 * node + 1 right_child = 2 * node + 2 return self.query(left_child, start, mid, left, right) + self.query(right_child, mid + 1, end, left, right) def update(self, node, start, end, index, value): if start == end: self.arr[index] = value self.tree[node] = value else: mid = (start + end) // 2 left_child = 2 * node + 1 right_child = 2 * node + 2 if start <= index and index <= mid: self.update(left_child, start, mid, index, value) else: self.update(right_child, mid + 1, end, index, value) self.tree[node] = self.tree[left_child] + self.tree[right_child] ``` 使用这个模板,可以通过以下步骤来构建和使用线段: 1. 创建一个 SegmentTree 对象,并传入原始数组作为参数。 2. 可以使用 `query` 方法来查询某个区间的和,传入查询区间的左右边界。 3. 可以使用 `update` 方法来更新原始数组中的某个元素,传入元素的索引和新的值。 注意,这只是一个简单的线段模板,可以根据具体问题的需求进行适当修改和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 7
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值