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

news2024/11/20 18:31:18

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

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

它们的时间复杂度都是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)  # 第一个参数是根节点的下标,第二个参数是要修改的位置,第三个参数是要修改的值

总结:

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

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

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

相关文章

ets Jan 8 2013,rst cause:1, boot mode:(3,6)解决esp8266不断崩溃重启!

1.RAM使用不要超过50% 2.usb直接插在电脑上&#xff0c;不要插在usb扩展坞上&#xff0c;防止电流太小造成开发板电量不够而引起的不断重启&#xff08;千万注意这个&#xff01;&#xff09; 3.减少全局变量使用 4.减少不必要的方法定义 5. //调试代码的时候打开 struct rst_i…

Three.js——基础材质、深度材质、法向材质、面材质、朗伯材质、Phong材质、着色器材质、直线和虚线、联合材质

个人简介 &#x1f440;个人主页&#xff1a; 前端杂货铺 &#x1f64b;‍♂️学习方向&#xff1a; 主攻前端方向&#xff0c;正逐渐往全干发展 &#x1f4c3;个人状态&#xff1a; 研发工程师&#xff0c;现效力于中国工业软件事业 &#x1f680;人生格言&#xff1a; 积跬步…

C#基础|构造方法相关

哈喽&#xff0c;你好&#xff0c;我是雷工。 以下为C#方法相关的学习笔记。 01 方法的概述 概念&#xff1a;方法表示这个对象能够做什么&#xff0c;也就是封装了这个对象行为。 类型&#xff1a;实例方法—>静态方法&#xff08;抽象方法、虚方法&#xff09;—>特殊…

【JAVA进阶篇教学】第五篇:Java多线程编程

博主打算从0-1讲解下java进阶篇教学&#xff0c;今天教学第五篇&#xff1a;Java多线程编程。 在Java编程中&#xff0c;使用多线程可以提高程序的并发性能&#xff0c;但是直接创建和管理线程可能会导致资源浪费和性能下降。Java提供了线程池来管理线程的生命周期和执行任务…

【蓝桥杯省赛真题40】python摘苹果 中小学青少年组蓝桥杯比赛 算法思维python编程省赛真题解析

目录 python摘苹果 一、题目要求 1、编程实现 2、输入输出 二、算法分析 三、程序编写 四、程序说明 五、运行结果 六、考点分析 七、 推荐资料 1、蓝桥杯比赛 2、考级资料 3、其它资料 python摘苹果 第十三届蓝桥杯青少年组python编程省赛真题 一、题目要求 &…

浅谈数据模型

1&#xff1a;事实表和维表的概述 前言&#xff1a;数据仓库是一种用于存储和管理大量数据的技术。其中&#xff0c;事实表和维表是数据仓库中的两个重要概念&#xff0c;首先了解一下事实表和维度表 1.事实表&#xff1a;是指用于存储测量“事实数据”的表&#xff0c;事实数…

IE浏览器,文件下载失败,onDownloadProgress方法里报错:无法获取未定义或null引用的属性“getResponseheader“

问题背景&#xff1a; 谷歌、火狐、edge都没有问题&#xff0c;ie11浏览器也没有问题&#xff0c;ie10及以下会报错&#xff0c;无法获取未定义或null引用的属性"getResponseheader 查看代码&#xff0c;getResponseHeader这个方法是在获取进度条的时候使用&#xff0c; …

OpenWRT设置自动获取IP,作为二级路由器

前言 上一期咱们讲了在OpenWRT设置PPPoE拨号的教程&#xff0c;在光猫桥接的模式下&#xff0c;OpenWRT如果不设置PPPoE拨号&#xff0c;就无法正常上网。 OpenWRT设置PPPoE拨号教程 但现在很多新装的宽带&#xff0c;宽带师傅为了方便都会把光猫设置为路由模式。如果你再外…

SpringCloud系列(15)--Eureka自我保护

前言&#xff1a;在上一章节中我们说明了一些关于Eureka的服务发现功能&#xff0c;也用这个功能进行接口的实现&#xff0c;在本章节则介绍一些关于Eureka的自我保护 1、Eureka保护模式概述 保护模式主要用于一组客户端和Eureka Server之间存在网络分区场景下的保护。默认情况…

带你走进不一样的策略模式

欢迎来到我的博客&#xff0c;代码的世界里&#xff0c;每一行都是一个故事 带你走进不一样的策略模式 前言策略模式简介概念解释 策略模式的结构策略模式优点项目实践之bean策略构思业务策略实现策略接口实现策略上下文业务实现 前言 在编程的世界里&#xff0c;每一次按键都…

Golang对接Ldap(保姆级教程:概念搭建实战)

Golang对接Ldap&#xff08;保姆级教程&#xff1a;概念&搭建&实战&#xff09; 最近项目需要对接客户的LDAP服务&#xff0c;于是趁机好好了解了一下。LDAP实际是一个协议&#xff0c;对应的实现&#xff0c;大家可以理解为一个轻量级数据库。用户查询。比如&#xff…

高频前端面试题汇总之Vue篇

1. Vue的基本原理 当一个Vue实例创建时&#xff0c;Vue会遍历data中的属性&#xff0c;用 Object.defineProperty&#xff08;vue3.0使用proxy &#xff09;将它们转为 getter/setter&#xff0c;并且在内部追踪相关依赖&#xff0c;在属性被访问和修改时通知变化。 每个组件实…

【ruoyi-vue】登录解析(后端)

调试登录接口 进入实现类可以有 验证码校验 登录前置校验 用户验证 验证码校验 通过uuid获取redis 中存储的验证码信息&#xff0c;获取后对用户填写的验证码数据进行校验比对 用户验证 1.进入控制器的 /login 方法 2.进入security账号鉴权功能&#xff0c;经过jar内的流…

算法-动态规划专题

文章目录 前言 : 动态规划简述1 . 斐波那契模型1.1 泰波那契数列1.2 最小花费爬楼梯1.3 解码方法 前言 : 动态规划简述 动态规划在当前我们的理解下,其实就是一种变相的递归,我们查看一些资料也可以知道,动态规划其实属于递归的一个分支,通过把递归问题开辟的栈帧通过一定的手…

代码随想录第45天|70. 爬楼梯 (进阶)322. 零钱兑换

70. 爬楼梯 &#xff08;进阶&#xff09; 57. 爬楼梯&#xff08;第八期模拟笔试&#xff09; (kamacoder.com) 代码随想录 (programmercarl.com) 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。 每次你可以爬至多m (1 < m < n)个台阶。你有多少种不同的方法可以爬到楼顶…

企业数字化转型,“业务”先行

在当今时代&#xff0c;数字化转型已经成为企业发展的必经之路。数字化转型&#xff0c;简而言之&#xff0c;就是运用数字技术&#xff0c;对企业运营管理的各个环节进行深度改造&#xff0c;以提升企业的运营效率和市场竞争力。据有关机构研究测算&#xff0c;数字化转型可使…

【静态分析】静态分析笔记07 - 指针分析基础

参考&#xff1a; 【课程笔记】南大软件分析课程7——指针分析基础&#xff08;课时9/10&#xff09; - 简书 -------------------------------------------------------------- 1. 指针分析规则 规则&#xff1a;采用推导形式&#xff0c;横线上面是条件&#xff0c;横线下…

Linux开发板配置静态IP

1、查看网口信息&#xff0c;易知eth0无IP地址 ifconfig2、首先分配一个IP地址 sudo ifconfig eth0 192.168.5.8 up3、此时配置的IP地址只是临时的&#xff0c;当你reboot重启板子上电后&#xff0c;ip地址会消失&#xff0c;因此需要为板子配置静态ip&#xff0c;避免每次上…

说方法不如传授经验向媒体投稿你可以这样

在信息爆炸的时代,作为单位的信息宣传员,肩负着将本单位的重要资讯、活动成果、政策解读等内容有效传播至公众视野的重任。其中,向各类媒体投稿无疑是实现这一目标的重要途径。然而,传统的邮件投稿方式常常让我深感力不从心,费时费力不说,成功率低、出稿慢等问题更是让我和领导…

SQL注入简单总结

一、SQL注入是什么 SQL注入即&#xff1a;是指web应用程序对用户输入数据的合法性没有判断或过滤不严&#xff0c;攻击者可以在web应用程序中事先定义好的查询语句的结尾上添加额外的SQL语句&#xff0c;在管理员不知情的情况下实现非法操作&#xff0c;以此来实现欺骗数据库服…