当前位置: 首页 > article >正文

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

   上一篇博客主要讲了线段树AcWing 1264. 动态求连续区间和 ,详细讲解线段树与树状数组(Python,篇一),那么本篇博客主要介绍一下什么是树状数组,它们的原理与结构是怎样,并通过实际题型来讲解。

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

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

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

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

什么是树状数组?

   顾名思义就是一个结构为树形结构的数组,于二叉树的结构类似但又不同,它是在二叉树的结构上删除了一些中间节点,来看两幅图就明白了.

1. 这是二叉树的结构

在这里插入图片描述

2. 这是树状数组的结构

在这里插入图片描述
   不难发现,树状数组相比于二叉树删除了一些节点,但是为什么要删除呢?这就和树状数组的一些性质(lowbit)有关了,不懂没关系,继续往下看.

前置知识—lowbit(x)运算

如何计算一个非负整数n在二进制下的最低位1及其后面的0构成的数?
例如: 44 = ( 101100 ) 2 44 = (101100 )_2 44=(101100)2 ,最低为1和后面的0构成的数是 ( 100 ) 2 = 4 (100)_2 = 4 (100)2=4
所以 l o w b i t ( 44 ) = l o w b i t ( ( 101100 ) 2 ) = ( 100 ) 2 = 4 lowbit(44) = l o w b i t ( ( 101100 )_2 ) = ( 100 )_2 = 4 lowbit(44)=lowbit((101100)2)=(100)2=4 ,那么lowbit运算时怎么实现的呢?

44的二进制=(101100),我们对44的二进制数取反+1,也即~44+1,得到-44

-44的二进制=(010100),然后我们把44和-44的二进制进行按位与运算,也即按位&得到,二进制000100,也就是十进制的4

所以lowbit(x) = x&(-x)

树状数组结构分析

   上文提到树状数组主要解决两个问题,单点修改区间查询。它和前缀和操作一样,但时间复杂度却相差甚大。前缀和总操作的时间复杂度是 O ( n ) O(n) O(n)而树状数组的时间复杂度是 O ( l o g n ) O(logn) O(logn)
在这里插入图片描述
   如图所示,树状数组中所有的奇数位都存的是自己本身(奇数位的二进制最后都是没有0,表示第0层),而对于偶数位,偶数位的二进制表示中末尾有几个0,就表示第几层。(以图中2,4,8 为例,2的二进制末尾有一个0,所以放在第一层,存放的内容是A[1]与A[2]的和。4放在第2层,存放前4个数的和)

   上面时树状数组的结构图,C[x]保存以x为根的子数中叶子节点值的和,原数组为A[]
那么原数组前4项的和C[4]=C[2]+C[3]+A[4]=C[1]+A[2]+C[3]+A[4]=A[1]+A[2]+A[3]+A[4],看似没有什么特点,别着急往下看

   我们通过观察节点的二进制数,进一步发现,树状数组中节点x的父节点为x+lowbit(x),例如C[2]的父节点为C[4]=C[2+lowbit(2)] (lowbit 函数:返回 x 的二进制表示中最低位的1对应的值)

单点修改,区间查询

   在进行单点修改的同时,需要更新其父节点,因为树状数组存的其实就是节点中所有子节点的和,例如我们对A[1]+k,那么祖先节点C[1],C[2],C[4],C[8]…都需要+k更新,此时我们就可以用lowbit操作实现.

# 定义 lowbit 函数:返回 x 的二进制表示中最低位的1对应的值
# 例如:lowbit(6)=2(6的二进制是 110,最低位的1对应 10)
def lowbit(x):
    return x & -x

# 定义树状数组的更新操作:在位置 x 增加 y
def add(x, y):
    # 从 x 开始,沿树状数组的父节点路径向上更新
    while x <= n:
        tree[x] += y
        x += lowbit(x)  # 移动到父节点

   那么单点修改实现了,如何实现区间查询呢?
   例如:我们需要查询前7项的区间和sum[7]

在这里插入图片描述
   通过图中不难看出,sum[7]=t[7]+t[6]+t[4] ,我们进一步发现,6=7-lowbit(7),4=6-lowbit(6),所以我们可以通过不断的-lowbit操作来实现求和

# 定义树状数组的前缀和查询:返回 [1, x] 区间的和
def query(x):
    res = 0
    # 从 x 开始,沿树状数组的子节点路径向下累加
    while x > 0:
        res += tree[x]
        x -= lowbit(x)  # 移动到左兄弟节点
    return res

   这只能求区间 [ 1 , x ] [1 , x] [1,x] 的区间和,那么如何求 [ L , R ] [L , R] [L,R] 的区间和呢?,这时候利用前缀和相减的性质就可以了, [ L , R ] = [ 1 , R ] − [ 1 , L − 1 ] [L,R]=[1,R]−[1,L−1] [L,R]=[1,R][1,L1]

   下面就是树状数组的经典模板题,大家一起来看一下吧。

题目: 动态求连续区间和

题目链接: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≤n≤100000 1n100000,
1 ≤ m ≤ 100000 1≤m≤100000 1m100000
1 ≤ a ≤ b ≤ n 1≤a≤b≤n 1abn,
数据保证在任何时候,数列中所有元素之和均在 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

思路

   本题就是树状数组的经典模板题,直接套用模板即可(上文中全部讲解过)。

代码及详细注释

# 读取输入:n 为数组长度,q 为操作次数
n, q = map(int, input().split())

# 数组初始化,索引从1开始(方便树状数组操作)
# num[0] 是占位符,实际数据从 num[1] 到 num[n]
num = [0] + list(map(int, input().split()))

# 树状数组初始化,大小为 n+1(索引从1到n)
tree = [0] * (n + 1)

# 定义 lowbit 函数:返回 x 的二进制表示中最低位的1对应的值
# 例如:lowbit(6)=2(6的二进制是 110,最低位的1对应 10)
def lowbit(x):
    return x & -x

# 定义树状数组的更新操作:在位置 x 增加 y
def add(x, y):
    # 从 x 开始,沿树状数组的父节点路径向上更新
    while x <= n:
        tree[x] += y
        x += lowbit(x)  # 移动到父节点

# 定义树状数组的前缀和查询:返回 [1, x] 区间的和
def query(x):
    res = 0
    # 从 x 开始,沿树状数组的子节点路径向下累加
    while x > 0:
        res += tree[x]
        x -= lowbit(x)  # 移动到左兄弟节点
    return res

# 初始化树状数组:将初始数据填充到树中
for i in range(1, n + 1):
    add(i, num[i])  # 将 num[i] 添加到位置 i

# 处理 q 次操作
while q:
    q -= 1
    # 读取操作类型和参数
    # k=0 表示区间查询 [a, b] 的和
    # k=1 表示在位置 a 增加 b
    k, a, b = map(int, input().split())
    
    if k == 0:
        # 计算区间和:sum([a, b]) = sum([1, b]) - sum([1, a-1])
        print(query(b) - query(a - 1))
    else:
        add(a, b)

http://www.kler.cn/a/553216.html

相关文章:

  • python-leetcode 37.翻转二叉树
  • 【c语言初阶】函数的嵌套
  • 143.WEB渗透测试-信息收集-小程序、app(14)
  • 【AIGC工具箱】AIGC重塑生活神器
  • chrome V3插件开发,调用 chrome.action.setIcon,提示路径找不到
  • 51单片机-外部中断
  • 【大厂AI实践】中原银行:中原银行 AI 平台建设实践
  • 大模型WebUI:Gradio全解11——使用transformers.agents构建Gradio UI(2)
  • Java与Go相比,有什么独特的优势
  • python 变量类型注释
  • SQL 注入攻击
  • 神经网络|(十)概率论基础知识-正态分布及python仿真
  • EasyExcel 自定义头信息导出
  • Linux-GlusterFS进阶分布式卷
  • 华为最新OD机试真题-通过软盘拷贝文件-Python-OD统一考试(E卷)
  • Vision Transformer图像分类实现
  • Activity 任务栈 taskAffinity 用法
  • 【机器学习与数据挖掘实战】案例14:基于随机森林分类器的汽车公司客户细分预测
  • CMU 15-445 23Fall Lab 总结
  • ​33页PDF | 基于数字化转型的数据指标与标签体系应用架构设计方案