线段树 Segment Tree

线段树 Segment Tree

  1. 前言

线段树是一类平衡二叉树,它实质上是对递归过程中处理数据的记忆,也就是说它把递归的步骤都进行及时记忆,及时递归结束,仍然可以查找或更新不同的递归区间。

线段树的结构与堆(二叉堆)结构比较类似,它的叶子结点储存的是原始数组中的元素,终结点储存对叶子结点的逐层处理的基本信息。

线段树的结点的储存方式可以采用两种基本类型,第一类结构和Heap结构比较类似,定义根节点的序列号v,那么其左子树的序列号定义为2*v,右子树的序列号定义为2*v +1,如果需要这类方式进行定义,那么需要我们从v=1开始构造线段树。

既然线段树属于平衡二叉树,构造线段树的信息就可以采用前序遍历,后续遍历或中序遍历的任何一种,一般情况下,采用约定成俗的后序遍历信息处理的方法。也就是,需要处理完成叶子节点后,最后处理父节点的信息,父节点的信息来自对叶子结点信息的基本处理。父节点的处理方式可以选择求和,求最大值或求最小值,甚至可以求解前缀和后缀的值。

  1. 线段树的实现和操作

我们从简单的问题开始,给定数组a[]={1,3,-2,8,7},需要查询任意区间段的和a[l…r],要求采用线段树的数据结构实现。如果数组元素比较少,可以采用每次递加的方式进行sum计算,但是当数据量很大的时候,时间复杂度和空间复杂度都非常高,不利于程序的执行。

那么我们就需要考虑采用线段树进行操作,之前提到线段树可以有效进行递归的分段记忆,在构造线段树之前,需要确定线段树元素的个数。

定义数组a[]元素个数为n,那么求和线段树得深度就为[log2n],如果当前深度为i,那么当前层的元素数量就为
2 i 2^i 2i
在这里插入图片描述

基于上述结论,那么线段树的元素总数描述为,

在这里插入图片描述

为了方便计算,我们需要把线段树的元素数量设定为4n,由于线段树并非满二叉树,所以4n元素当中有些元素并未充分填充,但是由于我们需要采用2*i和2*i+1的模式,必须预留4n的空间对线段树进行操作。

由于构造的线段树为相应的区间元素求和,所以每个元素中包含的基本信息为a[l…r]的元素和,对于叶子节点,由于l=r,所以叶子节点中储存的是数组元素的单个元素值;对于非叶子节点,我们需要对非叶子节点的左右孩子进行求和计算。涉及代码如下。

#define MAXN 10
int t[4*MAXN]; //segment tree data structure
void build(int *nums, int v, int tl, int tr)
{
    int tm;

    if (tl == tr) // int nums[]={1,3,-2,8,-7};
    {
        t[v]=nums[tl];
    }
    else
    {
        tm=(tl+tr)/2;

        build(nums,2*v,tl,tm);
        build(nums,2*v+1,tm+1,tr);
        t[v]=combine(t[2*v],t[2*v+1]);
    }
    return;}

int combine(int sum_1, int sum_2)
{
    return (sum_1+sum_2);
}

观察代码结构,可以明显看到线段树的构造过程为二叉树后续操作的过程, 分支语句build(nums,2*v,tl,tm)计算出左分支的值,分支语句build(nums,2*v+1,tm+1,tr)计算出右分支的具体值。两个分支的值计算出来之后,调用求和函数计算出父节点的值,不断重复,直至后序遍历 回到根节点为止。

线段树的另外一个重要的操作为查询操作(query),如果要查询某一区间段的求和,就需要对线段树再进行递归调用,整个过程中分为三类情况:

  • 查询区间位于左子树的某个区间(1)

  • 查询区间位于右子树的某个区间(2)

  • 查询区间横跨左右子树(3)

情形(1)

在这里插入图片描述

情形(2)

在这里插入图片描述

情形(3)

在这里插入图片描述

查询过程(query)可以理解为对线段树的递归遍历,类型属于后序遍历,在过程中不断对比tll && trr这两个条件是否成立,如果成立,则表明为情形(1)或情形(2)二者之一。对于情形(3),则需要左右两边的递归都返回后,然后再对结果进行合并。

int query(int v, int tl, int tr, int l, int r)
{
    int sum_leftmost;
    int sum_rightmost;
    int tm;

    if(l>r)
    {
        return 0;
    }
    else if(tl==l && tr==r)
    {
        return t[v];
    }
    else
    {
        tm=(tl+tr)/2;
        sum_leftmost=query(2*v,tl,tm,l,min(r,tm)); //essence of this program
        sum_rightmost=query(2*v+1,tm+1,tr,max(l,tm+1),r);

        return combine(sum_leftmost,sum_rightmost);
    }
}

如果l>r,则表明查询区间不包含l…r之间的计算结果,对于这个结点,我们不需要返回其本身的值,只要替代值0即可。

我们需要计算左半部分的查询结果和右半部分的查询结构,最后通过combine求解返回即可。下图表示计算区间[2,4]范围内的值的求和过程。

在这里插入图片描述

递归过程,当到结点4的时候由于l>r,所以直接返回0即可,接着遍历结点5,结点5满足tll && tr=r,直接返回t[4]即可,然后进行后续归并,0+(-2)=-2,整个过程相当于对结点5直接进行剔除,但是递归的过程保持不变。接着遍历结点3,由于结点3的tll3, trr4,我们直接返回其储存的值即可。最后再调用combine函数对其求和,返回最终的结果。

过程当中,我们使用 sum_leftmost和sum_rightmost来记录左右子树的具体值,从而不改变原有的线段树的值,从而达到搜索的目的。

最后一个操作是更新,在线段树的更新操作中,分为两类基本类型:

  • 对单点数据进行更新

  • 对区间段进行更新,对某个区间段内的所有元素进行相同的更新操作(区间内的每个元素的值增加x,或者对区间的计算结果进行重新赋值),还有一种情况是对区间段内储存的值进行相应更新

由于对区间的更新涉及到延迟数组或lazy[]数组的标记,本文暂不表述。我们仅针对单点的数据进行相应的更新,在函数中,我们需要指定待更新的位置上的具体数据,然后利用递归对所有的收到影响的结点进行相应的更新。

单点更新的过程实际是上剪枝遍历的过程,整个遍历过程中,需要比较中值位置和Pos的相对位置,以便决定递归是前往左子树还是右子树。

在这里插入图片描述

例如需要更新pos=2位置上的值,遍历的过程 根->左->右的具体过程,然后利用后序递归更新结点2的值为7,接着更新结点1的值为8.

实现代码也比较简单,

void update(int v, int tl, int tr, int pos, int new_value)
{
    int tm;
    if(tl==tr) // In this case, tl=tr=pos; condition will be met
    {
        t[v]=new_value;
    }
    else
    {
        tm=(tl+tr)/2;

        if(pos<=tm)
        {
            update(2*v,tl,tm,pos,new_value);
        }
        else
        {
            update(2*v+1,tm+1,tr,pos,new_value);
        }

        t[v]=combine(t[2 * v], t[2 * v + 1]);
    }
}
  1. 总结

线段树本质为递归过程中不同分割区间的信息的记忆,也可以是递归过程中信息处理的记忆,它把递归的区间信息映射到树上的索引编号,方便后续的查询(避免再次递归)。

对递归树的查询本质上是,对线段树的里面的信息再处理,处理的信息可能位于左子树,也可能位于右子树上,或者介于两者之间,但最终还是对这些信息的再组合以及再处理。

参考资料:

Segment Tree - Algorithms for Competitive Programming (cp-algorithms.com)

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值