线段树(原理,模板)

news2024/11/26 12:17:00

文章目录

  • 线段树
  • 线段树代码(单点修改、区间查询)
  • 懒惰标记与区间修改
  • 树状数组与区间修改

线段树

线段树是用来维护 区间信息 的数据结构

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

信息是否可以由线段树来维护,要看一段区间的信息,是否可以由它的子区间推导而来,显然加法,求最值都是满足这种性质的。从离散数学的角度来说,可以认为是满足幺半群性质的信息。对于有些复杂的题目,单一的信息可能并不能满足这一性质,而需要多个信息共同维护才能满足。

线段树的形态

将一段区间看做一个结点,长度为 10 的数组可以生成如下线段树。

父结点表示整个区间,将其划分为左右两个区间作为孩子,不断递归划分,直到区间长度为 1。

img

可以看到,线段树接近于一棵满二叉树,所以可以像堆一样,用一维数组来存储(即,若某一结点下标为 u,则其父结点下标为 u / 2,左孩子下标为 2 * u,右孩子下标为 2 * u + 1)。

一个长度为 n n n 的数组,所建立的线段树,倒数第二层接近有 n n n 个结点,将其以上看作满二叉树,则二叉树的高度为 log ⁡ 2 n + 1 \log_2n+1 log2n+1 ,共有 2 log ⁡ 2 n + 1 − 1 2^{\log_2n+1}-1 2log2n+11 个结点,化简得 2 n − 1 2n-1 2n1,最后一层最坏情况是倒数第二层的两倍,看作有 2 n 2n 2n 个点,所以估计最坏情况有 4 n − 1 4n-1 4n1 个点,所以我们开大小为 4 n 4n 4n 的数组。


  • 线段树一共有 5 个操作:
    • pushup 用子节点的信息来更新父结点
    • pushdown 向下分配懒惰标记(用于区间修改)
    • build 初始化线段树
    • query 查询
    • modify 修改

pushup 需要根据自己维护的区间信息来编写:如维护区间最大值,则父结点的区间最大值就是max(左孩子区间最大值, 右孩子区间最大值);如维护区间和,父结点区间和 = 左孩子区间和 + 右孩子区间和。

build 递归建树,基本模板:

注意左右子树初始化完成后需要 pushup 当前结点。

void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) return;
    int mid = (l + r) / 2;
    build(2 * u, l, mid);
    build(2 * u + 1, mid + 1, r);
    pushup(u);
}

query 查询区间

如图,如果我们要查询 [ 5 , 9 ] [5, 9] [5,9] 的信息,则最终需要 [ 5 ] , [ 6 , 8 ] , [ 9 ] [5],[6,8],[9] [5],[6,8],[9] 三个区间合并求得。(阴影标注的是递归需要经过的结点)

img

我们从根结点开始查询,要查询的区间和当前结点的孩子有下面三种情况:

  • 只跟左孩子有交集:则继续递归左孩子,不递归右孩子
  • 只跟右孩子有交集:则继续递归右孩子,不递归左孩子
  • 跟两个孩子都有交集:继续递归左右两个孩子

如果当前结点的区间完全在要查询的区间的内部,则直接返回当前结点的信息。

注意:不存在和两个孩子都没有交集的情况,因为如果和两个孩子都没有交集,则意味着和当前结点也没有交集,而要查询的区间一定和根结点有交集,递归只会向下找其和要查询的区间有交集的孩子,所以当前结点不可能和要查询的区间没有交集。

modify :单点修改只需要递归向下搜索,同时使用 pushup 回溯即可。

线段树代码(单点修改、区间查询)

一个基本的线段树有 4 个操作,可以支持区间查询和单点修改

以维护 区间最大值 为例,有如下代码:

  • tr[i]:编号为 i 的结点表示的区间为 [l, r],值为 v,根结点编号为 1
  • build:从上至下初始化线段树各个结点的区间,每个区间的值我们没有更新,因为这里默认原数组元素全为 0。
  • query:查询 [l,r] 区间,如果当前结点的区间包含在 [l,r] 里面,那么直接返回值即可,否则递归左右结点中和 [l,r] 有交集的区间。返回查询结果(max)。
  • modify:单点修改,将下标 x 位置修改为 v。先递归搜索 x 所在的区间,找到叶子结点直接修改即可,回溯的时候调用 pushup 函数来用子结点更新父结点。
const int N = 100010;

struct Node {
    int l, r;
    int v;
}tr[4 * N];

void pushup(int u) {
    tr[u].v = max(tr[2 * u].v, tr[2 * u + 1].v);
}

void build(int u, int l, int r) {
    tr[u] = {l, r};
    if (l == r) return;
    int mid = (l + r) / 2;
    build(2 * u, l, mid);
    build(2 * u + 1, mid + 1, r);
}

int query(int u, int l, int r) {
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].v;
    int mid = (tr[u].l + tr[u].r) / 2;
    int v = 0;
    if (l <= mid) v = query(2 * u, l, r);
    if (r > mid) v = max(v, query(2 * u + 1, l, r));
    return v;
}

void modify(int u, int x, int v) {
    if (tr[u].l == x && tr[u].r == x) tr[u].v = v;
    else {
        int mid = (tr[u].l + tr[u].r) / 2;
        if (x <= mid) modify(2 * u, x, v);
        else modify(2 * u + 1, x, v);
        pushup(u);
    }
}

懒惰标记与区间修改

懒惰标记可以通过延迟对结点的修改,减少操作次数。

当我们要执行修改时,可以使用 modify 将当前结点的信息进行更新,但不再向下递归,而是给当前结点打上懒惰标记,该标记表示,当前结点以下的所有子节点(不包括当前结点)都需要更新。当下一次访问到带有标记的结点的孩子之前,才对结点的孩子进行实质性的修改。在这样的设定下,根结点和当前结点的信息一定是最新的(正确的)。当然也可以让懒惰标记包括当前结点,这里所给出的是前一种代码。

以维护 区间和 为例,我们的懒惰标记就设置为 add,它是一个整型,表示该结点所表示的区间的每个元素都要加 add,其下的所有子节点都需要更新。

  • pushdown 用来下放懒惰标记,就是将当前结点的懒惰标记叠加到左右孩子的懒惰标记上,清空当前结点的懒惰标记,并对左右孩子进行实质性修改。

  • modify:将 [l, r] 区间的元素加上 d

    • 如果当前区间在 [l, r] 区间内部,则直接修改当前区间,并打上懒惰标记,不向下递归。
    • 否则,因为即将访问到需要进行实质性修改的子结点,所以需要先将当前结点的懒惰标记下放。否则就会导致 pushup 使用错误的子结点的信息来更新当前结点。

总结:每次(modify、query)递归子结点之前都要 pushdown,所有修改操作(build、modify)递归完子结点之后都要 pushup

const int N = 100010;

int w[N]; // 原数组
struct Node {
    int l, r;
    int sum, add;
}tr[4 * N];

void pushup(int u) {
    tr[u].sum = tr[2 * u].sum + tr[2 * u + 1].sum;
}

void pushdown(int u) {
    Node& root = tr[u], &left = tr[2 * u], &right = tr[2 * u + 1];
    if (root.add) {
        left.add += root.add;
        right.add += root.add;
        left.sum += (left.r - left.l + 1) * root.add;
        right.sum += (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) / 2;
        build(2 * u, l, mid);
        build(2 * u + 1, mid + 1, r);
        pushup(u);
    }
}

void modify(int u, int l, int r, int d) {
    if (l <= tr[u].l && tr[u].r <= r) {
        tr[u].sum += (tr[u].r - tr[u].l + 1) * d;
        tr[u].add += d;
    } else {
        pushdown(u);
        int mid = (tr[u].l + tr[u].r) / 2;
        if (l <= mid) modify(2 * u, l, r, d);
        if (r > mid) modify(2 * u + 1, l, r, d);
        pushup(u);
    }
}

int query(int u, int l, int r) {
    if (l <= tr[u].l && tr[u].r <= r) return tr[u].sum;
    pushdown(u);
    int mid = (tr[u].l + tr[u].r) / 2;
    int sum = 0;
    if (l <= mid) sum += query(2 * u, l, r);
    if (r > mid) sum += query(2 * u + 1, l, r);
    return sum;
}

树状数组与区间修改

上一节 树状数组(代码模板和原理详解)_世真的博客-CSDN博客 讲到,树状数组只支持单点修改和区间查询,不支持区间修改。

但是如果题目让我们对一个数组进行区间修改和求区间和,其实也可以使用树状数组。不同于线段树的是,线段树可以直接维护这个数组,而树状数组需要维护它的差分数组。

对差分数组的单点修改等价于对原数组的区间修改。

但是这又带来一个问题:对差分数组的区间求和,相当于求原数组的单点值,而我们要的是对原数组的区间求和,这怎么解决呢?

设原数组 a a a 内的一个前缀区间的元素为 a 1 , a 2 , ⋯   , a x a_1,a_2,\cdots,a_x a1,a2,,ax,其对应的差分为 b 1 , b 2 , ⋯   , b x b_1,b_2,\cdots,b_x b1,b2,,bx


∑ i = 1 x a i = ∑ i = 1 x ∑ j = 1 i b j \sum_{i=1}^xa_i=\sum_{i=1}^x\sum^i_{j=1}b_j i=1xai=i=1xj=1ibj
把各项列出来(黑色部分):

img

将三角补全成一个完整的矩阵(红色部分)

黑色部分等于整个矩阵的和减去红色部分
( b 1 + b 2 + b 3 + ⋯ + b x ) × ( x + 1 ) − ( b 1 + 2 b 2 + 3 b 3 + ⋯ + x b x ) (b_1+b_2+b_3+\cdots+b_x)\times(x+1)-(b_1+2b_2+3b_3+\cdots+xb_x) (b1+b2+b3++bx)×(x+1)(b1+2b2+3b3++xbx)
这个式子就是用 b i b_i bi 的前缀和,乘 x + 1 x+1 x+1 后减去 i b i ib_i ibi 的前缀和。

所以我们需要维护两个数组,分别是差分数组 b i b_i bi i b i ib_i ibi 数组。

代码如下:

const int N = 100010;

int n, m;
int a[N];
int tr1[N];
int tr2[N];

int lowbit(int x) {
    return x & -x;
}

void add(int tr[], int x, int c) {
    for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;
}

int sum(int tr[], int x) {
    int res = 0;
    for (int i = x; i; i -= lowbit(i)) res += tr[i];
    return res;
}

int prefix_sum(int x) {
    return sum(tr1, x) * (x + 1) - sum(tr2, x);
}

int range_sum(int l, int r) {
    return prefix_sum(r) - prefix_sum(l - 1);
}

void range_add(int l, int r, int c) {
    add(tr1, l, c);
    add(tr1, r + 1, -c);
    add(tr2, l, l * c);
    add(tr2, r + 1, (r + 1) * -c);
}

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

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

相关文章

Reasoning Through Memorization: Nearest Neighbor Knowledge Graph Embeddings论文阅读

研究问题 将基于记忆的方法与预训练语言模型相结合&#xff0c;以完成知识图谱补全任务 背景动机 传统模型无法处理未见实体记忆增强神经网络的相关进展&#xff0c;即在传统的计算模块之外添加单独的记忆存储模块 模型方法 首先使用预训练语言模型构建实体的知识库&#…

计算机网络第一章 计算机网络与因特网

1.0 目录[TOC]1.1 什么是Internet&#xff1f;1.1.1 最细微&#xff1a;图&#xff08;Graph&#xff09;Internet由结点Node和边Edge组成结点Node分为主机结点和交换结点边Edge分为接入网链路Access和主干链路Backbone结构图如下&#xff1a;1.1.2 网络的网络&#xff08;Netw…

【Python】数值计算基础

note scipy和numpy库可以便捷地进行科学计算&#xff0c;如线性代数、常微分方程数值求解、信号处理、图像处理、稀疏矩阵等。 文章目录note一、多项式基础1. 1 多项式表示和拟合1.2 多项式插值二、微积分计算2.1 数值积分2.2 符号积分三、矩阵运算3.1 线性方程组的求解3.2 矩…

PHP代码审计之MVC与ThinkPHP简介

今天继续给大家介绍渗透测试相关知识&#xff0c;本文主要内容是PHP代码审计之MVC与ThinkPHP简介。 免责声明&#xff1a; 本文所介绍的内容仅做学习交流使用&#xff0c;严禁利用文中技术进行非法行为&#xff0c;否则造成一切严重后果自负&#xff01; 再次强调&#xff1a;严…

文化向技术投降

《技术垄断:文化向技术投降》泼斯曼 技术发展三个阶段 1&#xff0c;工具使用文化 2&#xff0c;技术统治文化 3&#xff0c;技术垄断文化 趣讲大白话&#xff1a;科技是一把双刃剑 泛滥的信息已经把人给弄懵了 *********** 广义上来讲&#xff0c;公司是技术公司 才有可能有更…

Windows压缩工具 “ Bandizip 与 7-zip ”

前言 &#x1f4dc;“作者 久绊A” 专注记录自己所整理的Java、web、sql等&#xff0c;IT技术干货、学习经验、面试资料、刷题记录&#xff0c;以及遇到的问题和解决方案&#xff0c;记录自己成长的点滴 目录 前言 一、什么是压缩 二、Bandizip的简介 1、大概介绍 2、详细…

Acwing---1238.日志统计

日志统计1.题目2.基本思想3.代码实现1.题目 小明维护着一个程序员论坛。现在他收集了一份”点赞”日志&#xff0c;日志共有 NNN 行。 其中每一行的格式是&#xff1a; ts id 表示在 tststs 时刻编号 ididid 的帖子收到一个”赞”。 现在小明想统计有哪些帖子曾经是”热帖…

一起自学SLAM算法:9.3 SVO算法

连载文章&#xff0c;长期更新&#xff0c;欢迎关注&#xff1a; 下面将从原理分析、源码解读和安装与运行这3个方面展开讲解SVO算法。 9.3.1 SVO原理分析 前面已经说过&#xff0c;SVO算法是半直接法的典型代表。因此在下面的分析中&#xff0c;首先介绍一下半直接法的基本原…

网络攻防中监控某个IP的流量和数据分析

网络攻防中监控某个IP的流量和数据分析。 Windows 可以使用 tcpview 工具监控某个IP的流量信息&#xff0c;Linux 可以使用iftop 工具。 新版本的 tcpview 带过滤功能&#xff0c;可以对 IP 进行过滤。最后两列显示的是对应程序发送和接收的字节数。 tcpview 工具下载地址&am…

【Quicker】您的指尖工具箱

在日常学习和工作中我们常常用到各种各样的小工具&#xff0c;比如&#xff1a;截图并编辑、取色、文字识别、公式识别等等.   倘若这每一项功能都下载一个程序&#xff0c;则会显得非常冗杂。因此&#xff0c;用一个工具箱将这些功能集合起来&#xff0c;则是一个不错的解决…

机器自动翻译古文拼音 - 十大宋词 - 满江红 怒发冲冠 南宋·岳飞

满江红 怒发冲冠 南宋岳飞 怒发冲冠&#xff0c;凭栏处&#xff0c;潇潇雨歇。 抬望眼&#xff0c;仰天长啸&#xff0c;壮怀激烈。 三十功名尘与土&#xff0c;八千里路云和月。 莫等闲&#xff0c;白了少年头&#xff0c;空悲切。 靖康耻&#xff0c;犹未雪。臣子恨&#x…

点云 3D 分割 - RangeNet++(IROS 2019)

点云 3D 分割 - RangeNet&#xff08;IROS 2019&#xff09;摘要1. 引言2. 相关工作3. 我们的方法A. 距离图像点云代理表示B. 完全卷积语义分割C. 基于距离图像的点云重建D. 高效点云后处理4. 实验评价A. RangeNet相对于最新技术的性能B. 消融研究C. 后处理影响D. 运行时5. 结论…

JavaWeb | 预编译SQL及PreparedStatement讲解

本专栏主要是记录学习完JavaSE后学习JavaWeb部分的一些知识点总结以及遇到的一些问题等&#xff0c;如果刚开始学习Java的小伙伴可以点击下方连接查看专栏 本专栏地址&#xff1a;&#x1f525;JDBC Java入门篇&#xff1a; &#x1f525;Java基础学习篇 Java进阶学习篇&#x…

JavaSE总结

JavaSE目录初识JavaJava由来main 方法介绍Java程序的运行数据类型和变量数据类型基本数据类型引用数据类型运算符算术运算符关系运算符逻辑运算符移位运算逻辑控制方法方法的重载与重写关于递归数组二维数组类和对象成员变量成员方法对象this 关键字构造方法封装代码块内部类非…

ext文件系统

Ext文件系统 1.文件目录 1.1 文件控制块FCB 文件系统通过文件控制块&#xff08;File Control Blcok&#xff09;来维护文件结构&#xff0c;FCB包含有关文件的信息&#xff0c;包括所有者、权限、文件内容的位置等文件目录用于组织文件&#xff0c;每个目录项对应一个FCB文…

(考研湖科大教书匠计算机网络)第三章数据链路层-第三节:差错控制

专栏目录首页&#xff1a;【专栏必读】考研湖科大教书匠计算机网络笔记导航 文章目录一&#xff1a;检错编码&#xff08;1&#xff09;奇偶校验码&#xff08;2&#xff09;循环冗余检验码&#xff08;CRC&#xff09;二&#xff1a;纠错编码&#xff08;海明校验码&#xff0…

Opencv调参神器——trackBar控件

Opencv调参神器——trackBar控件 调参需求介绍trackBar控件介绍trackBar控件使用函数案例一:trackBar控件调整图片颜色案例二:trackBar控件调整Canny算子参数案例三:trackBar控件调整图像融合参数trackBar控件总结调参需求介绍 想要学好计算机视觉,有一个库必不可少,那就…

ARM Makefile 基础

一、Makefile 的作用和意义 (1) 工程项目中 c 文件太多管理不方便&#xff0c;因此用 Makefile 来做项目管理&#xff0c;方便编译链接过程。 (2) uboot 和 linux kernel本质上都是 C 语言的项目&#xff0c;都由很多个文件组成&#xff0c;因此都需要通过 Makefile 来管理。…

nodejs小区物业管理系统vue前端

目 录 1 概述 1 1.1课题背景及意义 1 1.2 国内外研究现状 1 1.3 本课题主要工作 2 2 系统开发环境 3 前端技术&#xff1a;nodejsvueelementui 前端&#xff1a;HTML5,CSS3、JavaScript、VUE 1、 node_modules文件夹(有npn install产生) 这文件夹就是…

STM32-Modbus-RTU-01-05-15功能码补充-波特率在线修改-断电数据保护

文章目录一、本文主要内容二、使用modbus通信协议在线修改STM32波特率&#xff08;一&#xff09;STM32标准库在线修改串口波特率&#xff08;二&#xff09;STM32HAL库-485-modbus-rtu通信在线修改串口波特率1、STM32F103ZET6芯片(1)HAL库下参考标准库形式修改波特率(2)直接修…