AcWing 1264. 动态求连续区间和 ,详细讲解线段树与树状数组(Python,篇一)

本篇博客主要介绍一下什么是线段树与树状数组,它们的原理与结构是怎样,并通过实际题型来讲解,篇一主要讲解线段树,下一篇博客讲解树状数组。

线段树与树状数组的区别和特点:

它们的时间复杂度都是O(nlogn)

  • 存储方式和空间复杂度不同。线段树使用树形结构存储,其空间复杂度通常较高;树状数组使用数组存储,空间复杂度较低。
  • 操作复杂度不同。线段树和树状数组在操作复杂度上有所差异,线段树的查询和更新操作的时间复杂度通常为O(log n)或O(log^2 n),而树状数组的查询和更新操作的时间复杂度为O(log n)。
  • 应用场景不同。线段树适用于区间修改、区间查询的场景;树状数组适用于单点修改、区间查询的场景。
  • 功能不同。线段树可以维护区间信息,包括区间和、最大值、最小值等;树状数组主要维护前缀和,通过特定操作也可以实现区间查询,但功能上不如线段树强大。

总体来说,线段树的构造更难一些,但是功能很强,树状数组的实现较简单,但功能较弱

什么是线段树?

自己写了半天的博客发现还是水平有限,介绍的知识点不太全面,这里引用一篇其他博主的线段树介绍什么是线段树,介绍的内容很细也很好理解。

这里说明一下问什么要开4n倍的数组空间:
设最后有n个叶结点,对应的满二叉树最多有2n个叶结点(这是因为极端情况是倒数第二层区间长度1,2交替) 然后根据(2n)+n+n/2…<=4n

下面结合具体题目来看看如何用线段树解决实际问题。

题目: 动态求连续区间和

题目链接:1264. 动态求连续区间和

给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b]
的连续和。

输入格式
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。
第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。数列从 1 开始计数。

输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,
数据保证在任何时候,数列中所有元素之和均在 int 范围内。

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8

输出样例:

11
30
35

思路:

题目描述很简单,看数据范围是 1 0 5 10^5 105,如果每次都要遍历数组再查询的话时间复杂度为O( n 2 n^2 n2),也就是 1 0 10 10^{10} 1010,明显超时。所以需要把时间复杂度降到O(logn),而题目中涉及的操作只有区间修改和区间查询,线段树的模板题。

首先需要构建线段树,每个节点有3个值,左区间,右区间,区间总和值

class Node:
    def __init__(self):
        # 左右区间与总和
        self.l, self.r, self.sum = 0, 0, 0

构造线段树其实就是数据结构中构造树的过程

def push_up(u):  # 利用它的两个儿子来算一下它的当前节点信息
    # 左儿子 u * 2 ,右儿子 u * 2 + 1
    tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum


def build(u, l, r):  # 第一个参数:当前节点编号。第二个参数:左边界。第三个参数:右边界。
    if l == r:  # 如果当前已经是叶节点了,那我们就直接赋值就可以了
        tr[u].l, tr[u].r, tr[u].sum = l, r, val[r]

    # 否则的话,说明当前区间长度至少是 2 对吧,那么我们需要把当前区间分为左右两个区间,那先要找边界点
    else:
        tr[u].l, tr[u].r = l, r  # 这里记得赋值一下左右边界的初值
        mid = (l + r) // 2  # 边界的话直接去计算一下 l + r 的下取整
        build(u * 2, l, mid)  # 先递归一下左儿子
        build(u * 2 + 1, mid + 1, r)  # 然后递归一下右儿子
        push_up(u)  # 做完两个儿子之后的话呢 push_up 一遍u ,更新一下当前节点信息


build(1, 1, n)  # 第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n

如样例所示线段树图示:
区间 [1,10]
数列值为 [1, 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
在这里插入图片描述

线段树构造好之后就要进行修改和查询操作了,这里的查询无需用到 lazy 标记,因为它不是修改完所有数值之后才进行查询操作,而是修改和查询操作随时都会进行不分先后顺序。

查询操作:

def query(u, l, r):  # 查询的过程是从根结点开始往下找对应的一个区间
    if tr[u].l >= l and tr[u].r <= r:  # 如果当前区间已经完全被包含了,那么我们直接返回它的值就可以了
        return tr[u].sum
    # 否则的话我们需要去递归来算
    else:
        mid = (tr[u].l + tr[u].r) // 2  # 计算一下我们 当前 区间的中点是多少
        total_sum = 0  # 用 total_sum 来表示一下我们的总和
        if mid >= l:  # 看一下我们当前区间的中点和左边有没有交集
            total_sum += query(u * 2, l, r)
        if mid + 1 <= r:  # 看一下我们当前区间的中点和右边有没有交集
            total_sum += query(u * 2 + 1, l, r)
        return total_sum

这里有两处稍微绕一点:

  1. 查询区间和时,为什么当前区间被完全包含就返回它的值?
  2. 查询区间和时,为什么要判断跟左bb右边有无交集?

这里举个例子求一下就可以明白

我们在查询时,查询的过程是从根结点开始往下找对应的一个区间,如果要查找的区间是根节点区间,直接返回,否则第一次递归之前是不可能有区间完全包含根节点区间,拿样例结合上图图示说明,l 和 r 的取值范围就在 1 ~ 10 之间,不可能小于1 也不可能大于 10。

只要不是根节点区间查询区间和,那么就会进行到下一步。比如查询区间 [1, 8] 的和,因为查询区间不完全包含根节点区间,所以需要判断根节点区间的中点跟查询区间左右两边是否有交集。

根节点区间的中点为5,所以查询区间 [1, 8] 跟左右两边都有交集,所以总和total_sum 需要加上跟左右两边交集的和(也就是要加上[1,5] 和 [6,8]的和)。

先加左半边交集的和,递归到左孩子节点(下标索引为2的节点),因为[1, 8] 完全包含[1,5] 所以直接返回 区间[1,5] 的和(和是15)。

再加右半边交集的和,继续判断,直到可以求出[6,8] 的和为止。

修改操作:

def modify(u, index, v):  # 第一个参数也就是当前节点的编号,第二个参数是要修改的位置,第三个参数是要修改的值
    if tr[u].l == tr[u].r:  # 如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了
        tr[u].sum += v
    else:
        mid = (tr[u].l + tr[u].r) // 2
        # 看一下 index 是在左半边还是在右半边
        if index <= mid:  # 如果是在左半边,那就找左儿子
            modify(u * 2, index, v)
        else:  # 如果在右半边,那就找右儿子
            modify(u * 2 + 1, index, v)
        # 更新完之后当前节点的信息就要发生变化对吧,那么我们就需要 push_up 一遍
        push_up(u)

以上就是线段树的所有操作,就是按照线段树的概念来构成的,理解了线段树的概念也就会做本题了。

完整代码及注释:

class Node:
    def __init__(self):
        # 左右区间与总和
        self.l, self.r, self.sum = 0, 0, 0


def push_up(u):  # 利用它的两个儿子来算一下它的当前节点信息
    # 左儿子 u * 2 ,右儿子 u * 2 + 1
    tr[u].sum = tr[u * 2].sum + tr[u * 2 + 1].sum


def build(u, l, r):  # 第一个参数:当前节点编号。第二个参数:左边界。第三个参数:右边界。
    if l == r:  # 如果当前已经是叶节点了,那我们就直接赋值就可以了
        tr[u].l, tr[u].r, tr[u].sum = l, r, val[r]

    # 否则的话,说明当前区间长度至少是 2 对吧,那么我们需要把当前区间分为左右两个区间,那先要找边界点
    else:
        tr[u].l, tr[u].r = l, r  # 这里记得赋值一下左右边界的初值
        mid = (l + r) // 2  # 边界的话直接去计算一下 l + r 的下取整
        build(u * 2, l, mid)  # 先递归一下左儿子
        build(u * 2 + 1, mid + 1, r)  # 然后递归一下右儿子
        push_up(u)  # 做完两个儿子之后的话呢 push_up 一遍u ,更新一下当前节点信息


def query(u, l, r):  # 查询的过程是从根结点开始往下找对应的一个区间
    if tr[u].l >= l and tr[u].r <= r:  # 如果当前区间已经完全被包含了,那么我们直接返回它的值就可以了
        return tr[u].sum
    # 否则的话我们需要去递归来算
    else:
        mid = (tr[u].l + tr[u].r) // 2  # 计算一下我们 当前 区间的中点是多少
        total_sum = 0  # 用 total_sum 来表示一下我们的总和
        if mid >= l:  # 看一下我们当前区间的中点和左边有没有交集
            total_sum += query(u * 2, l, r)
        if mid + 1 <= r:  # 看一下我们当前区间的中点和右边有没有交集
            total_sum += query(u * 2 + 1, l, r)
        return total_sum


def modify(u, index, v):  # 第一个参数也就是当前节点的编号,第二个参数是要修改的位置,第三个参数是要修改的值
    if tr[u].l == tr[u].r:  # 如果当前已经是叶节点了,那我们就直接让他的总和加上 v 就可以了
        tr[u].sum += v
    else:
        mid = (tr[u].l + tr[u].r) // 2
        # 看一下 index 是在左半边还是在右半边
        if index <= mid:  # 如果是在左半边,那就找左儿子
            modify(u * 2, index, v)
        else:  # 如果在右半边,那就找右儿子
            modify(u * 2 + 1, index, v)
        # 更新完之后当前节点的信息就要发生变化对吧,那么我们就需要 push_up 一遍
        push_up(u)


n, m = map(int, input().split())
val = list(map(int, input().split()))
val = [0, *val]  # 记录一下权重
tr = [Node() for _ in range(4 * n + 10)]  # 记得开 4 倍空间,防止爆栈
build(1, 1, n)  # 第一个参数是根节点的下标,根节点是一号点,然后初始区间是 1 到 n
for _ in range(m):
    k, a, b = map(int, input().split())
    if k == 0:
        print(query(1, a, b))  # 求和的时候,也是传三个参数,第一个的话是根节点的编号 ,第二和第三个的话是我们查询的区间
    else:
        modify(1, a, b)  # 第一个参数是根节点的下标,第二个参数是要修改的位置,第三个参数是要修改的值

总结:

没接触线段树前还觉得线段树很难实现和理解,但其实把线段树的概念原理理解了就会发现线段树还是比较简单的(就是不太好构造,树形结构肯定不如普通数组好构造)。下一篇博客将讲解树状数组。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

使用navicate演示在 PostgreSQL 中使用 for 循环语句

1、简单循环示例 do $$ beginfor cnt in 1..10 loopraise notice cnt: %, cnt;end loop; end; $$ navicate中执行 2、循环查询 do $$ declare_record record; beginfor _record in (SELECT version,description FROM flyway_schema_history ORDER BY installed_rank desc li…

ios CI/CD 持续集成 组件化专题一 iOS 将图片打包成bundle

一、 创建 选择 macos 下的Bundle 二 、取名点击下一步 三、Base SDK 选择ios 四 、Build Active Architecture Only 五、Installation后面的内容删除 六、Skip Install 选择NO 七、Strip Debug Symbols During Copy 中"Release"项设置为 "YES" 八、COM…

书生·浦语 大模型(学习笔记-7)LMDeploy 量化部署 LLM-VLM 实践

目录 一、模型的部署 二、模型部署面临的问题 三、如何解决&#xff08;两种方法&#xff09; 四、LMDeploy相关知识 创建conda环境(漫长的等待) 五、使用LMDeploy与模型对话 六、设置最大KV Cache缓存大小 七、W4A16量化 八、客户端连接API服务器 一、模型的部署 二、…

leetcode-二叉树的镜像-91

题目要求 思路1 1.遍历一遍二叉树&#xff0c;将左边的结点对应创建一个右边的结点 2.用此方法空间复杂度O(n)&#xff0c;并不是最优 思路2 1.将一个结点的左右子树进行交换&#xff0c;如果左子树还有左右结点&#xff0c;就再交换左子树的左右结点&#xff0c;以此递归下去…

【树莓派】yolov5 Lite,目标检测,行人检测入侵报警

延续之前的程序&#xff1a; https://qq742971636.blog.csdn.net/article/details/138172400 文章目录 播放声音pygame不出声音怎么办&#xff08;调节音量&#xff09;树莓派上的音乐播放器&#xff08;可选&#xff09;命令行直接放歌&#xff08;尝试放mp3歌曲&#xff09; …

linux 上 jps 列出一堆 jar,如何快速定位 jar 文件启动位置?

例如&#xff0c;在 /data下有一个 xxx.jar &#xff0c;如果是通过 "java -jar /data/xxx.jar" 方式启动&#xff0c;则 jps会列出的名字中带 xxx.jar&#xff0c;这时再 "ps -ef | grep xxx.jar" 就会列出 更详细的信息&#xff0c;例如 "java -ja…

[iOS]CocoaPods安装和使用

1.了解brew、rvm、ruby、gem、cocaspods之间的关系 在 macOS 环境中&#xff0c;Brew、RVM、Ruby、Gem 和 CocoaPods 之间存在以下关系&#xff1a; Homebrew (Brew)&#xff1a;Homebrew 是 macOS 上的包管理器&#xff0c;用于安装和管理各种开源软件包。它使您能够轻松地从…

Windows 本地直接使用 SSH,SFTP 以及 SFTP下载文件到 Windows/mac 本地或上传(没有客户端时)

windows 本地打开 ssh 以及 sftp 等的方式 1.win(windows图标那个键) r 直接搜 然后从打开的位置运行 如果是打开 sftp 前面的 ssh 换一下成sftp 就行 直接从地址栏输入也可以直接转过去 通过 windows 的工具直接访问 sftp 后将文件下载到自己的windows 或 mac 上 先通过…

微软在汉诺威工业博览会上推出新制造业Copilot人工智能功能,强化Dynamics 365工具集

在近日于德国汉诺威举行的盛大工业博览会上&#xff0c;微软向全球展示了其最新推出的制造业人工智能功能&#xff0c;这些功能以Dynamics 365工具集为核心&#xff0c;旨在通过先进的AI技术为制造业带来前所未有的变革。 此次推出的新功能中&#xff0c;最为亮眼的是支持AI的…

Linux之ebpf(1)基础使用

Linux之ebpf(1)基础使用 Author: Once Day Date: 2024年4月20日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文章可以参考专栏&#xff1a;Linux基础知识_Once-D…

Linux系统网络---DNS域名解析服务

目录 一、DNS的简介 DNS系统的分布式数据结构&#x1f447; DNS系统类 两种查询方式 二.正向解析实验 1.先关闭防火墙、selinux 2.安装bind 3.查看配置、修改配置 4.修改区域配置文件 正向解析&#x1f447; 反向解析&#x1f447; 5.修改 正向解析&#x1f…

装饰品模式介绍

装饰器模式是一种结构型设计模式&#xff0c;它允许用户在不改变现有对象的情况下向一个对象添加新的功能。在 Java 中&#xff0c;装饰器模式经常用来动态地给对象添加额外的行为&#xff0c;如日志记录、事务管理、安全检查等。 装饰器模式涉及四个主要角色&#xff1a;组件&…

公司服务器中的kafka消息中间件挂了,我是如何修复的?

今天的公司的system系统服务在运行过程中&#xff0c;提示连接不上kafuka的消息中间件。但是负责kafka的同事已经离职了&#xff0c;询问公司开发也不知道如何处理&#xff0c;我是如何重启kafka消息中间件使system系统服务正常运行&#xff1f; 查看kafka的安装位置 在下面的…

【C++】---STL之list的模拟实现

【C】---STL之list的模拟实现 一、list模拟实现思路二、结点类的实现三、list迭代器的实现1、ListIterator类2、构造函数3、operator*运算符重载5、operator->运算符重载6、operator&#xff01;运算符重载7、operator运算符重载8、前置9、后置10、前置--11、后置-- 四、lis…

从零开始安装 stable diffusion webui v1.9.3 (windows10)

从零开始安装 stable diffusion webui v1.9.3 (windows10) CUDA 安装 CUDA 12.1 | https://developer.nvidia.com/cuda-toolkit-archive CUDNN 8.x | https://developer.nvidia.com/rdp/cudnn-archive 安装路径 F:/CUDA/v12.1 安装git git官网 | https://git-scm.com/ 安…

Linux文件/目录高级管理一(头歌实训)

目录 任务描述 相关知识 Linux修改文件权限命令 Linux修改所有者权限 Linux修改同组用户权限 Linux修改其他用户权限 编程要求 任务描述 相关知识 Linux修改目录权限命令 Linux修改所有者权限 Linux修改同组用户权限 Linux修改其他用户权限 编程要求 任务描述 相…

3.1设计模式——Chain of Responsibility 责任链模式(行为型)

意图 使多个对象都有机会处理请求&#xff0c;从而避免请求的发送者和接受者之间的耦合关系&#xff0c;将这些对象练成一条链&#xff0c;并沿着这条链传递请求&#xff0c;直到有一个对象处理它为止。 实现 其中 Handle定义一个处理请求的接口&#xff1a;&#xff08;可选…

【线段树 区间位运算模板】3117划分数组得到最小的值之和

本文涉及知识点 线段树 区间位运算模板 LeetCode3117. 划分数组得到最小的值之和 给你两个数组 nums 和 andValues&#xff0c;长度分别为 n 和 m。 数组的 值 等于该数组的 最后一个 元素。 你需要将 nums 划分为 m 个 不相交的连续 子数组&#xff0c;对于第 ith 个子数组…

机器视觉系统-工业光源什么是低角度打光方式

光路描述&#xff1a;光线与水平面角度 <45称为低角度光。 效果分析&#xff1a;低角度照射&#xff0c;被侧物表面平整部分的反射光无法进入入镜头&#xff0c;图像效果表现为灰度值较低&#xff1b;不平整部分的反射光进入镜头&#xff0c;图像效果表现为灰度值较高。 主要…

【白盒测试】单元测试的理论基础及用例设计技术(6种)详解

目录 &#x1f31e;前言 &#x1f3de;️1. 单元测试的理论基础 &#x1f30a;1.1 单元测试是什么 &#x1f30a;1.2 单元测试的好处 &#x1f30a;1.3 单元测试的要求 &#x1f30a;1.4 测试框架-Junit4的介绍 &#x1f30a;1.5 单元测试为什么要mock &#x1f3de;️…