广告
返回顶部
首页 > 资讯 > 后端开发 > 其他教程 >C++ 线段树原理与实现示例详解
  • 953
分享到

C++ 线段树原理与实现示例详解

2024-04-02 19:04:59 953人浏览 安东尼
摘要

目录一、问题引入二、线段树的构建三、线段树的单点修改与查询1、修改2、查询四、线段树的区间修改与查询1、修改2、查询一、问题引入 对于一般的区间问题,比如RMQ(区间的最值)、区间的

一、问题引入

对于一般的区间问题,比如RMQ(区间的最值)、区间的和,如果使用朴素算法,即通过遍历的方式求取,则时间复杂度为O(N),在常数次查询的情况下可以接受,但是当区间长度为N,查询次数为M时,查询复杂度就变成O(M*N)。在M和N较大时,这样的复杂度无法满足要求。

对于这类问题,有一个神奇的数据结构,能够在O(M*logN)的时间内解决问题——线段树。

二、线段树的构建

线段树的每个节点可以根据需要存储一个区间的最大/最小值/和等内容。它的构建方式与堆的构建方式类似,即线段树是基于数组实现的树。

以构建区间和的线段树为例:对于给定数组nums,设大小为n,则区间范围为[0, n-1]。

  • 规定线段树的根节点,即SegmentTree[0]存储[0, n)的和。
  • 根据堆的构建方法,父节点的左孩子为2*parent+1,右孩子为2*parent+2。
  • 假设父节点存储[start, end]的和,mid=start+(end - start>>1),则左孩子存储[start, mid]的和,右孩子存储[mid+1, end]的和
  • 注:mid=start+(end - start>>1)是一种避免整形溢出的写法,等价于mid=(start+end)/2
  • 由于父节点的值依赖于两个子节点,因此线段树的构建是一种后序遍历
// nums是给定大小为n的数组,par表示当前正在构建的线段树节点下标,start和end是当前需要计算的区间。
void build(vector<int>& nums, int par, int start, int end)
{
    if (start == end) // 区间大小为1,即单个点,因此当前节点的区间和就是单点的值
    {
        _segmentTree[par] = nums[start];
        return;
    }
    // 如果区间大于1,则先求当前节点的左孩子和右孩子
    int mid = start + (end - start >> 1);
    int lchild = 2 * par + 1;
    int rchild = 2 * par + 2;
    build(nums, lchild, start, mid);   // 递归求左节点的区间和
    build(nums, rchild, mid + 1, end); // 递归求右孩子的区间和
    // 当前节点的值就是左孩子的值+右孩子的值
    _segmentTree[par] = _segmentTree[lchild] + _segmentTree[rchild];
}

注:在极端情况下,最后一层有n个结点,此时线段树是一棵完全二叉树,树的高度h=log2N向上取整+1≤log2N+2。

因此,树的节点数量为2^h-1^≤2^logN+2^-1=4N-1。

所以,线段树数组的大小一般为4*n

此外,如果想要避免因为n过大而导致MLE,则可以选择map/unordered_map来存储线段树,不过这会增加时间成本。一般来说直接开辟4*n的线段树数组是最方便书写的。

三、线段树的单点修改与查询

1、修改

单点修改要求:修改原数组下标index处的值。此时我们需要对线段树进行更新:

  • 依然是从根节点开始进行修改。
  • 根据修改的下标index,判断应当修改当前节点的左子树还是右子树。
  • 在递归修改左右孩子节点以后,再根据左右孩子的值重新对父节点进行赋值(pushUp())。
void update(int index, int val, int par, int start, int end)
{
    if (start == end) // 递归结束条件依然是当前区间为单点
    {
        segtree[par] = val;
        return;
    }
    int mid = start + (end - start >> 1);
    // 递归修改左孩子或右孩子
    if (index <= mid)
        update(index, val, 2 * par + 1, start, mid);
    else
        update(index, val, 2 * par + 2, mid + 1, end);
    // 修改完成后重新对父节点赋值
    pushUp(par);
}
// pushUp负责利用左右孩子的值更新父节点
void pushUp(int par)
{
    segtree[par] = segtree[2 * par + 1] + segtree[2 * par + 2];
}

2、查询

由于每个节点可以存储最值和区间和,因此求最值与求和的过程几乎相同,这里以求和为例:

假设当前节点的区间为[start, end],中点为mid。

对于给定区间[left, right],它有三种分布情况:

  • right<=mid,即给定区间全部在左节点中,因此只需要递归左子树计算区间和即可。
  • left>mid,即给定区间全部在右节点中,因此只需要递归右子树计算区间和即可。
  • 给定区间有一部分在左子树,一部分在右子树,因此需要分成两部分,一部分是[left, mid],这部分到左子树中递归求取。另一部分是[mid+1,right],这部分到右子树中递归求取。
// [left, right]是目标求和区间,par是当前节点编号,当前节点存储区间[start, end]的和
int query(int left, int right, int par, int start, int end)
{
    // 目标求和区间与当前节点的区间吻合,直接返回当前节点的值即可
    if (left == start && right == end)
        return segtree[par];
    int mid = start + (end - start >> 1);
    if (right <= mid) // 目标求和区间全部在左子树
        return query(left, right, 2 * par + 1, start, mid);
    else if (left > mid) // 目标求和区间全部在右子树
        return query(left, right, 2 * par + 2, mid + 1, end);
    else  // 目标求和区间分布在左右子树上
        return query(left, mid, 2 * par + 1, start, mid) +
               query(mid + 1, right, 2 * par + 2, mid + 1, end);
}

四、线段树的区间修改与查询

1、修改

区间修改要求:修改原数组[left, right]处的值,将它们全部加/减value,或者全部改为value。此时我们需要对线段树进行更新。

我们可以选择将[left, right]看成一个个点,然后进行单点修改,但是一个点的修改消耗为log2N,修改整个区间就是C*log2N了,M次修改就是M*C*log2N,这比暴力法的M*C还要差。

我们使用懒标记法,引入一个lazy变量:

依然从根节点开始修改。

如果节点对应的区间[start, end]完全包含在[left, right]中时,即left≤start≤end≤right,此时将这个节点的值进行修改,并按要求修改lazy,比如:对给定区间整体加4,则lazy加4,整体减3,则lazy减3。

修改完lazy数组后,我们不再需要修改它的子节点,因此lazy的意义在于减少向下更新的次数,从而降低时间复杂度**「懒的体现」**。

如果节点对应的区间[start, end]不完全包含在[left, right]中时,则递归修改左右节点,直至对应节点的区间与待修改的区间没有交集**「递归的结束条件」**。子树修改完成后,再利用子节点的值更新父节点(pushUp())。

注意:由于lazy变量的存在,使用子节点的值更新父节点时,需要加上父节点的lazy值,因为该值是由于"偷懒"而没有添加在子节点上的。

// 以「将给定区间内的数加x,查询每个节点存储对应区间的和」为例:
void update(int left, int right, int x, int node, int start, int end)
{
    // 区间没有交集,无需修改
    if (end < left || right < start)
        return;
    // 当前节点对应的区间被需要修改的区间完全包含
    if (left <= start && right >= end)
    {
        segtree[node].val += x * (end - start + 1);
        segtree[node].lazy += x;
        return;
    }
    // 不被[left, right]完全包含,则说明本轮只会更新[start, end]的一部分,因此不能再"偷懒"直接将x加在lazy上了
    // 而是先根据lazy的值修改左右子节点,然后再递归修改左右子树
    int mid = start + ((end - start) >> 1);
    // 先利用lazy修改孩子节点
    pushDown(node, mid - start + 1, end - mid);
    // 递归修改孩子节点
    update(left, right, 2 * node + 1, start, mid);
    update(left, right, 2 * node + 2, mid + 1, end);
    // 利用左右子树的区间最大值确定父节点的区间最大值
    pushUp(par);
}
void pushUp(int par)
{
	segtree[par].val = segtree[2 * par + 1] + segtree[2 * par + 2] + segtree[par].lazy;
}
// par表示父节点,ln表示左孩子的区间长度,rn表示右孩子的区间长度
void pushDown(int par, int ln, int rn)
{
    if (segtree[par].lazy != 0)
    {
        segtree[2 * par + 1].val += segtree[par].lazy * ln; // 修改左孩子的值
        segtree[2 * par + 1].lazy += segtree[par].lazy; // 偷懒,不再往下继续修改,因此左孩子继承父节点的lazy值
        segtree[2 * par + 2].lazy += segtree[par].lazy * rn;
        segtree[2 * par + 2].lazy += segtree[par].lazy;
        segtree[par].lazy = 0; // 父节点的lazy已经分配到子节点了,因此父节点lazy清零
    }
}

2、查询

查询的过程与修改几乎相同:

  • 依然从根节点开始查询。
  • 如果当前节点有懒标记,此时返回节点的值,无需向下遍历。
  • 当某个节点对应的区间[start, end]完全包含在[left, right]中时,即left≤start≤end≤right,则该节点的值是我们最终结果的子集,直接返回节点值即可。
  • 如果不完全包含,则递归查询左右子树,直至对应节点的区间与待修改的区间没有交集**「递归的结束条件」**。利用子树的查询结果作为最终的返回结果。
// 以「将给定区间内的数加x,查询每个节点存储对应区间的和」为例:
bool query(int left, int right, int node, int start, int end)
{
    // 区间没有交集,无需查询
    if (end < left || right < start)
        return false;
    // 有懒标记,则无需查询左右孩子,而是直接返回节点值,外加懒标记
    // 或者当前节点对应的区间被需要查询的区间完全包含,则直接返回节点值
    if (segtree[node].lazy || left <= start && right >= end)
        return segtree[node].val;
    int mid = start + ((end - start) >> 1);
    // 不完全包含,则先根据lazy修改子节点,再递归查询左右子树的和
    pushDown(node, mid - start + 1, end - mid);  
    return query(left, right, 2 * node + 1, start, mid) +
           query(left, right, 2 * node + 2, mid + 1, end);
}
// par表示父节点,ln表示左孩子的区间长度,rn表示右孩子的区间长度
void pushDown(int par, int ln, int rn)
{
    if (segtree[par].lazy != 0)
    {
        segtree[2 * par + 1].val += segtree[par].lazy * ln; // 修改左孩子的值
        segtree[2 * par + 1].lazy += segtree[par].lazy; // 偷懒,不再往下继续修改,因此左孩子继承父节点的lazy值
        segtree[2 * par + 2].lazy += segtree[par].lazy * rn;
        segtree[2 * par + 2].lazy += segtree[par].lazy;
        segtree[par].lazy = 0; // 父节点的lazy已经分配到子节点了,因此父节点lazy清零
    }
}

以上就是c++ 线段树原理与实现示例详解的详细内容,更多关于C++ 线段树原理的资料请关注编程网其它相关文章!

--结束END--

本文标题: C++ 线段树原理与实现示例详解

本文链接: https://www.lsjlt.com/news/167651.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

本篇文章演示代码以及资料文档资料下载

下载Word文档到电脑,方便收藏和打印~

下载Word文档
猜你喜欢
  • C++ 线段树原理与实现示例详解
    目录一、问题引入二、线段树的构建三、线段树的单点修改与查询1、修改2、查询四、线段树的区间修改与查询1、修改2、查询一、问题引入 对于一般的区间问题,比如RMQ(区间的最值)、区间的...
    99+
    2022-11-13
  • 线段树详解以及C++实现代码
    目录应用场景算法思想查询操作修改操作算法实现建树查询修改总结应用场景 假设有这样的问题:有n个数,m次操作,操作分为:修改某一个数或者查询一段区间的值 分析下,如果针对数组元素的修改...
    99+
    2022-11-12
  • C语言详细讲解树状数组与线段树
    目录树状数组动态求连续区间和数星星线段树动态求连续区间和数列区间最大值树状数组 动态求连续区间和 给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,...
    99+
    2022-11-13
  • C++实现AVL树的示例详解
    目录AVL 树的概念AVL 树的实现节点的定义接口总览插入旋转AVL 树的概念 也许因为插入的值不够随机,也许因为经过某些插入或删除操作,二叉搜索树可能会失去平衡,甚至可能退化为单链...
    99+
    2023-03-03
    C++实现AVL树 C++ AVL树
  • C语言树状数组与线段树实例分析
    这篇文章主要介绍“C语言树状数组与线段树实例分析”,在日常操作中,相信很多人在C语言树状数组与线段树实例分析问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”C语言树状数组与线段树实例分析”的疑惑有所帮助!接下来...
    99+
    2023-06-30
  • Java数据结构之线段树的原理与实现
    目录简介实现思路节点定义构建线段树求解区间和更新线段树简介 线段树是一种二叉搜索树,是用来维护区间信息的数据结构。可以在O(logN)的时间复杂度内实现单点修改、区间修改、区间查询(...
    99+
    2022-11-13
  • C++递归与分治算法原理示例详解
    目录1. 汉诺塔问题2. 全排列问题3. 利用递归与分治策略寻找最大值4. 归并排序5. 快速排序6. 棋盘覆盖问题1. 汉诺塔问题   递归算法,分为 3 步:...
    99+
    2022-11-12
  • C/C++函数原理传参示例详解
    目录x84-64的寄存器函数是个什么东西?一个简单的函数传参姿势入栈规则看看汇编全都存寄存器吗?传对象呢?x84-64的寄存器 本文所用gcc为 x86-64 gcc 10.1 w...
    99+
    2022-12-08
    C/C++ 函数原理传参 C/C++ 函数传参
  • 线程池的原理与实现详解
    一. 线程池的简介通常我们使用多线程的方式是,需要时创建一个新的线程,在这个线程里执行特定的任务,然后在任务完成后退出。这在一般的应用里已经能够满足我们应用的需求,毕竟我们并不是什么...
    99+
    2022-11-15
    线程池 原理
  • RESTful API设计原则与实现示例详解
    目录1. 什么是REST?2. RESTful API设计原则3. 实现RESTful API4. 总结1. 什么是REST? 在本文中,我们将详细讲解RESTful API的设计原...
    99+
    2023-05-16
    RESTful API设计原则 RESTful API
  • C++存储链接性原理示例详解
    目录链接性外部链接性单定义规则内部链接性无链接性总结链接性 链接性是指名称在不同文件之间能否共享,而作用域是指名称在文件内部哪些范围可见。 这里的文件并非开发时创建的文件,而是将文...
    99+
    2023-01-03
    C++存储链接性原理 C++ 存储链接性
  • javaReentrantLock条件锁实现原理示例详解
    目录引言条件锁的使用ReentrantLock.newCondition()Condition.awaitCondition.signal引言 在前两篇文章中,我们了解了Reentr...
    99+
    2023-01-09
    java ReentrantLock条件锁 java ReentrantLock
  • c#实现flv解析详解示例
    下面是一个使用C#实现FLV解析的示例代码:```csharpusing System;using System.IO;public...
    99+
    2023-08-16
    C#
  • C++实现与Lua相互调用的示例详解
    目录概述示例C++调用lua编译lua流程运行lua调用C++解释运行调用语义概述 从本质上来看,其实说是不存在所谓的C++与lua的相互调用。lua是运行在C上的,简单来说lua的...
    99+
    2023-05-13
    C++ Lua相互调用 C++ Lua 调用 C++ Lua
  • java编程FinalReference与Finalizer原理示例详解
    之前写了一篇java编程Reference核心原理示例源码分析的文章,但由于篇幅和时间的原因没有给出FinalReference和Finalizer的分析。同时也没有说明为什么建议不...
    99+
    2022-11-13
  • C语言实例实现二叉搜索树详解
    目录有些算法题里有了这个概念,因为不知道这是什么蒙圈了很久。 先序遍历: root——>left——>right 中序遍历...
    99+
    2022-11-13
  • Go底层channel实现原理及示例详解
    目录概念:使用场景:底层数据结构:操作:创建发送接收关闭案例分析:概念: Go中的channel 是一个队列,遵循先进先出的原则,负责协程之间的通信(Go 语言提倡不要通过共享内存来...
    99+
    2022-11-11
  • SpringBootStarter依赖原理与实例详解
    目录1 Starter2 了解 spring.factories机制2.1 不同包路径下的依赖注入2.2 spring.factories 机制3 spring.factories ...
    99+
    2022-11-13
  • 详解C# ConcurrentBag的实现原理
    目录一、ConcurrentBag类二、 ConcurrentBag线程安全实现原理2.1、ConcurrentBag的私有字段2.2、用于数据存储的ThreadLocalList类...
    99+
    2022-11-12
  • Python学习之二叉树实现的示例详解
    Python实现二叉树 Python实现二叉树可以使用面向对象编程的方式,通过定义二叉树节点类来实现。每个节点包含一个数据元素、左右子节点指针和一些操作方法,如插入节点、查找节点、...
    99+
    2023-05-15
    Python实现二叉树 Python二叉树
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作