线段树详解 原理解释 + 构建步骤 + 代码(带模板)

news2024/11/16 7:24:40

目录

介绍:    

定义:

以具体一个题目为例:​编辑

树的表示方法:

实现步骤:

构建结点属性:

pushup函数:

build函数:

pushdown函数:

modify函数:

query函数:

如何记忆:

模板:


介绍:    

        线段树(Segment Tree)是一种常用的数据结构,用于解决涉及区间查询的问题。它主要用于在数组或列表等数据结构上支持以下两类查询操作:

  1. 区间查询:查询某个区间内的统计信息,例如求和、最大值、最小值等。
  2. 区间更新:修改数组中某个区间元素的值,并相应地更新线段树中的信息。

        核心思想是将原始数据递归地划分成一系列不相交的区间,并在每个区间上维护一些预先计算好的信息,以支持高效的区间查询。

定义:

        假设我们有一个包含 N 个元素的数组 A,线段树 T 是基于数组 A 的线段树。线段树 T 是一个满二叉树,它具有以下性质:

  1. 根节点表示整个数组的区间 [1, N]。
  2. 如果一个节点表示的区间是 [left, right],则它的左子节点表示的区间是 [left, mid],右子节点表示的区间是 [mid+1, right],其中 mid 是 left 和 right 的中间值。
  3. 叶子节点表示数组 A 中的单个元素,而内部节点表示对应区间上的预计算信息(如区间和、区间最大值等)。
  4. 线段树通常使用数组来模拟实现。

线段树算法一般包含以下个函数:

        1.build(); 初始构建一个线段树。

        2.pushpu(); 向上传递信息。

        3.pushdown(); 向下传递懒标记,并且更新子树。

        4.modify(); 修改某一区间。

        5.queru(); 查询某一区间信息。

下面我们一个一个来介绍。 

以具体一个题目为例:

下面解析以此题目为例子。

树的表示方法:

        我们用 tr 数组来模拟这颗树。

        假设根节点在 tr 数组中的的下标为为 i,那么其左右子树的下标为:

                左:i * 2 (i << 1)

                右:i * 2 + 1 (i << 1 | 1)

        我们一般使用位运算,也就是括号里的,含义是一样的。所以可以计算出,tr 数组的长度最多就是题目所给数组长度的4倍。

实现步骤:

事先把输入的数组存在 w数组 中。

构建结点属性:

        树结点其实就是一个区间,所以属性包含:左右边界,懒标记。

        此题的懒标记就是区间需要加上的值 d 。

        根据题目我们还需要查询区间的元素和,所以在其中添加一个 sum。

struct Node
{
    int l, r;
    LL sum;
    LL add; // 懒标记
}tr[N * 4];;

pushup函数:

        我们在 build 一颗树之前,要先写 pushup 函数,用于向上传递信息,因为我们只知道叶子节点的值,我们要用后序遍历去构建父亲,所以要用到 pushup ,根据题目,我们要向上传递的信息显然是左右子树的 sum 和,这样就可以算出父亲的 sum 。

void pushup(int u) // 向上传递信息
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}

build函数:

        接下来我们开始构建这颗树,若区间内只有一个元素(l == r),说明我们找到了叶子节点,给叶子节点赋值,若不是叶子节点(l != r),就继续向左右子树递归,在递归完成时(后序遍历)使用pushup,通过已经获得值的子树去更新父亲。

void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l, r, w[l], 0}; // 叶子节点
    else
    {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 若不是叶子节点,向下递归
        pushup(u); // 通过子树构建父亲
    }
}

pushdown函数:

        pushdown函数是给子树传递懒标记的,如果懒标记不为空,就将父亲的懒标记传递给左右子树,并且通过懒标记更新左右子树的信息,此题求的是sum,所以子树的 sum 值就要加上区间长度乘上父亲的懒标记,最后清空父亲的懒标记。

        注意:懒标记表示的子树需要添加的信息,不包含父亲自己,所以在传递懒标记时,才要传递懒标记同时更新子树。

void pushdown(int u)
{
    auto &root = tr[u], &left = tr[u << 1], & right = tr[u << 1 | 1];
    if (root.add)
    {
        // 传递懒标记并且更新子树
        left.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;
        right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;
        root.add = 0; // 删除懒标记
    }
}

modify函数:

        修改区间信息,如果当前遍历的节点区间已经在区间中,那么就直接给其加上懒标记,并且计算更新其 sum。如果当前遍历的节点区间中的一部分是需要修改的区间,那么就先向下传递懒标记pushdown,然后在向需要修改的左右子树去递归,后序返回时,要给更新父亲pushup。

void modify(int u, int l, int r, int v)
{
    // 结点在要修改的区间中
    if (l <= tr[u].l && r >= tr[u].r)
    {
        tr[u].sum += (tr[u].r - tr[u].l + 1) * v;
        tr[u].add += v; // 加上懒标记
    }
    else
    {
        pushdown(u); // 先传递懒标记
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, v);
        if (r > mid) modify(u << 1 | 1, l, r, v);
        pushup(u); // 更新父亲
    }
}

query函数:

        用于查询区间信息,这里就是查询区间的sum。若遍历到的节点区间在查询区间之中,就返回其sum,若节点区间只有一部分在查询区间中,一样的,也是先传递懒标记,然后继续向需要计算的左右子树去递归,后序返回时计算结果。

LL query(int u, int l, int r)
{
    if (l <= tr[u].l && r >= tr[u].r) return tr[u].sum; // 返回区间信息
    pushdown(u); // 也是先传递懒标记
    LL v = 0;
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) v = query(u << 1, l, r);
    if (r > mid) v += query(u << 1 | 1, l, r);
    return v;
}

如何记忆:

        最重要的是注意每个函数pushup,pushdown函数的位置。只有在modify函数才两个一起用。

build函数只用一个pushup,query函数只用一个pushdown。

模板:

根据具体题目,自行修改。

// 操作是给区间每一个数加d
// 询问是求某一区间和
#include<iostream>

using namespace std;

typedef long long LL;
const int N = 100010;

int w[N];
int n, m;

struct Node
{
    int l, r;
    LL sum;
    LL add; // 懒标记
}tr[N * 4];;

void pushup(int u) // 向上传递信息
{
    tr[u].sum = tr[u << 1].sum + tr[u << 1 | 1].sum;
}
void pushdown(int u)
{
    auto &root = tr[u], &left = tr[u << 1], & right = tr[u << 1 | 1];
    if (root.add)
    {
        // 传递懒标记并且更新子树
        left.add += root.add, left.sum += (LL)(left.r - left.l + 1) * root.add;
        right.add += root.add, right.sum += (LL)(right.r - right.l + 1) * root.add;
        root.add = 0; // 删除懒标记
    }
}
void build(int u, int l, int r)
{
    if (l == r) tr[u] = {l, r, w[l], 0}; // 叶子节点
    else
    {
        tr[u] = {l, r};
        int mid = l + r >> 1;
        build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r); // 若不是叶子节点,向下递归
        pushup(u); // 通过子树构建父亲
    }
}
void modify(int u, int l, int r, int v)
{
    // 结点在要修改的区间中
    if (l <= tr[u].l && r >= tr[u].r)
    {
        tr[u].sum += (tr[u].r - tr[u].l + 1) * v;
        tr[u].add += v; // 加上懒标记
    }
    else
    {
        pushdown(u); // 先传递懒标记
        int mid = tr[u].l + tr[u].r >> 1;
        if (l <= mid) modify(u << 1, l, r, v);
        if (r > mid) modify(u << 1 | 1, l, r, v);
        pushup(u); // 更新父亲
    }
}

LL query(int u, int l, int r)
{
    if (l <= tr[u].l && r >= tr[u].r) return tr[u].sum; // 返回区间信息
    pushdown(u); // 也是先传递懒标记
    LL v = 0;
    int mid = tr[u].l + tr[u].r >> 1;
    if (l <= mid) v = query(u << 1, l, r);
    if (r > mid) v += query(u << 1 | 1, l, r);
    return v;
}

int main()
{
    scanf("%d%d", &n, &m);
    for (int i = 1; i <= n; ++i) scanf("%d", &w[i]); // 读入数组
    build(1, 1, n); // 以1为根节点,1~n区间建树

    char op[2];
    int l, r, t;
    
    // 读入修改和查询,q是查询,否则是修改
    while (m -- )
    {
        scanf("%s%d%d", op, &l, &r);
        if (*op == 'Q') printf("%lld\n", query(1, l, r));
        else
        {
            scanf("%d", &t);
            modify(1, l, r, t);
        }
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/807827.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《零基础入门学习Python》第071讲:GUI的终极选择:Tkinter8

虽然我们能用 tkinter 设计不少东西了&#xff0c;但是不少同学还是感觉对这个界面编程掌控得还不够多&#xff0c;说白了&#xff0c;就是我们现在还没办法随心所欲的去绘制我们想要的界面&#xff0c;但是不瞒你说&#xff0c;今天的这一节课将会给你的人生乃至人生观带来翻天…

Linux下在终端输入密码隐藏方法

Linux系统中&#xff0c;如何将在终端输入密码时将密码隐藏&#xff1f; 最近做简单的登录界面时&#xff0c;不做任何操作的话&#xff0c;在终端输入密码的同时也会显示输入的密码是什么&#xff0c;这样对于隐蔽性和使用都有不好的体验。那么我就想到将密码用字符*隐藏起来…

Glow: Generative Flow with Invertible 1×1 Convolutions论文解析及实现(二)

Glow: Generative Flow with Invertible 11 Convolutions 代码github: https://github.com/rosinality/glow-pytorch添加链接描述 1 模型架构如下 1.1 左边图flow模型 Flow model ① ActNorm ② InvConv2dLU ③ AffineCoupling 1.2 右边模型结构Glow模型 Glow Model Block…

C++之poll与epoll总结(一百六十九)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

SpringCloudAlibaba之Ribbon

Ribbon是nacos自带的负载均衡器&#xff0c;属于客户端的负载均衡 但是在Spring高级版本中让LoadBalancer替代了 本人用的是2.1.0的nacos&#xff0c;ribbon还没有被替换。 使用&#xff1a; 在配置类中&#xff1a;LoadBalanced BeanLoadBalancedpublic RestTemplate restT…

ETHERNET/IP转RS485/RS232网关什么是EtherNet/IP?

网络数据传输遇到的协议不同、数据互通麻烦等问题&#xff0c;一直困扰着大家。然而&#xff0c;现在有一种神器——捷米JM-EIP-RS485/232&#xff0c;它将ETHERNET/IP网络和RS485/RS232总线连接在一起&#xff0c;让数据传输更加便捷高效。 那么&#xff0c;它是如何实现这一功…

Python Web开发技巧VIII

目录 ModelSerializer和Serializer区别是什么 从queryset中取出某个models的字段值 Q对象进行模糊匹配 HTTP方式-如何模糊搜索JSON字段中的某个KEY值呢&#xff1f; showmigrations 合并两个或多个queryset ModelSerializer和Serializer区别是什么 都是DRF中用于序列化和…

尚医通9:医院列表功能+GateWay网关

内容介绍 1、医院列表功能&#xff08;接口&#xff09; 4、医院列表功能&#xff08;前端&#xff09; 5、更新医院上线状态功能 6、医院详情 7、GateWay网关 8、医院排班管理需求分析 9、查看医院所有科室接口、前端 医院列表功能&#xff08;接口&#xff09; 接口…

代码随想录算法训练营day15 | 102. 二叉树的层序遍历,226. 翻转二叉树,101. 对称二叉树

目录 102. 二叉树的层序遍历 226. 翻转二叉树 101. 对称二叉树 100. 相同的树 100是101的衍生题目。572也为101的衍生题目。 102. 二叉树的层序遍历 思路&#xff1a; 以前的笔记 代码&#xff1a; class Solution {public List<List<Integer>> levelOrder(T…

web自动化测试-PageObject 设计模式

为 UI 页面写测试用例时&#xff08;比如 web 页面&#xff0c;移动端页面&#xff09;&#xff0c;测试用例会存在大量元素和操作细节。当 UI 变化时&#xff0c;测试用例也要跟着变化&#xff0c; PageObject 很好的解决了这个问题。 使用 UI 自动化测试工具时&#xff08;包…

Reinforcement Learning with Code 【Chapter 9. Policy Gradient Methods】

Reinforcement Learning with Code This note records how the author begin to learn RL. Both theoretical understanding and code practice are presented. Many material are referenced such as ZhaoShiyu’s Mathematical Foundation of Reinforcement Learning, . 文章…

C++之实例化对象总结(一百七十)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 人生格言&#xff1a; 人生…

13 亿美金买个寂寞?No!AI 时代的数据行业蓄势待发

6月底&#xff0c;全球数据分析领域彻底炸锅了。 两大数据分析企业Databricks和Snowflake纷纷将目光瞄准了AI大模型。要知道&#xff0c;这两位对手平时没少对台戏&#xff0c;为性能、产品和技术经常开撕。但在今年的自家大会上&#xff0c;两家企业却出奇的一致&#xff0c;…

opencv+ffmpeg环境(ubuntu)搭建全面详解

一.先讲讲opencv和ffmpeg之间的关系 1.1它们之间的联系 我们知道opencv主要是用来做图像处理的&#xff0c;但也包含视频解码的功能&#xff0c;而在视频解码部分的功能opencv是使用了ffmpeg。所以它们都是可以处理图像和视频的编解码&#xff0c;我个人感觉两个的侧重点不一…

【博客684】Multi-regional高可用模式部署VictoriaMetrics

Multi-regional模式部署VictoriaMetrics 整体架构图 每个工作负载区域&#xff08;地球、火星、金星&#xff09;都有一个 vmagent&#xff0c;通过监控设置将数据发送到多个区域。监控设置&#xff08;地面控制 1,2&#xff09;包含 VictoriaMetrics 时间序列数据库 (TSDB) 集…

四姑娘山三日游

趁着小孩放暑假&#xff0c;从昆明回来之后&#xff0c;直接自驾到四姑娘山。 第一天 成都-四川省阿坝藏族羌族自治州小金县日隆镇(20230711) 大概9:30从成都市郫都区出发&#xff0c;路线如下&#xff1a;郫都—都江堰–映秀—耿达—卧龙—四姑娘山&#xff0c;中途翻过巴朗…

Notepad++工具通过正则表达式批量替换内容

1.每行末尾新增特定字符串 CtrlH弹出小窗口&#xff1b;查找目标输入$&#xff0c;替换为输入特定字符串&#xff1b;选中循环查找&#xff0c;查找模式选正则表达式&#xff1b;最后点击全部替换 2.每行行首新增特定字符串 CtrlH弹出小窗口&#xff1b;查找目标输入^&…

会议OA之我的会议(会议排座送审)

目录 前言&#xff1a; 2.我的会议&#xff1a; 2.1实现的特色功能&#xff1a; 2.2思路&#xff1a; 2.3功能实现&#xff1a; 我的会议页面&#xff1a;myMeeting.jsp myMeeting.js Dao方法 在mvc中配置info信息 Meeting InfoAction 2.4会议排座的思路&#xff1a; …

第四代SHARC® ADSP-21479KBCZ-2A、ADSP-21479BSWZ-2A、ADSP-21479KSWZ-2A高性能DSP(数字信号处理器)

第四代SHARC Processors 现在内置低功耗浮点DSP产品&#xff08;ADSP-21478和ADSP-21479&#xff09;&#xff0c;可提供改进的性能、基于硬件的滤波器加速器、面向音频与应用的外设以及能够支持单芯片解决方案的新型存储器配置。所有器件都彼此引脚兼容&#xff0c;而且与以往…

【Android知识笔记】UI体系(二)

什么是UI线程? 常说的UI线程到底是哪个线程?UI线程一定是主线程吗? 下面先给出两条确定的结论: UI线程就是刷新UI所在的线程UI是单线程刷新的关于第二条为什么UI只能是单线程刷新的呢?道理很简单,因为多线程访问的话需要加锁,太卡,所以一般系统的UI框架都是采用单线程…