树状数组总结

news2024/11/22 21:37:39

【使用总结】

经典用法:单点更新o(logn),区间查询/区间最大值(1~n,求sum),o(logn)

扩展用法:区间修改,如对[x,y]区间加上一个数k

模板题:力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台

class BinaryIndexTree:

    def __init__(self, array: list):
        '''初始化,总时间 O(n)'''
        self._array = [0] + array
        n = len(array)
        #每遍历一个节点加到其父节点上
        for i in range(1, n + 1):
            j = i + (i & -i)
            if j < n + 1:
                self._array[j] += self._array[i]

    def lowbit(self, x: int) -> int:
        return x & (-x)

    def update(self, idx: int, val: int):
        '''将原数组idx下标更新为val, 总时间O(log n)'''
        prev = self.query(idx, idx + 1)    # 计算出原来的值
        idx += 1
        val -= prev    # val 是要增加的值
        while idx < len(self._array):
            self._array[idx] += val
            idx += self.lowbit(idx)

    def query(self, begin: int, end: int) -> int:
        '''返回数组[begin, end) 的和'''
        return self._query(end) - self._query(begin)

    def _query(self, idx: int) -> int:
        '''计算数组[0, idx)的元素之和'''
        res = 0
        while idx > 0:
            res += self._array[idx]
            idx -= self.lowbit(idx)
        return res

【知识点】
树状数组,是一种小巧优雅的数据结构,可在 O(logn) 的时间内计算出数列的前缀和。树状数组,又称二进制索引树。
树状数组的经典实现包含两个数组:一个是存储数列元素的数组 A[],另一个是存储数列前缀和的数组 C[]。而树状数组名称的由来,恰是因为数组 C[] 呈现为树状结构。两个数组之间的关系为:C[i]=A[i-2^k+1]+A[i-2^k+2]+…+A[i],其中的 k 表示 i 的二进制表示末尾有k个连续的 0 。且由 C[] 与 A[] 的关系式易得,每个 C[i] 由数组 A[] 中的 i-(i-2^k+1)+1=2^k 个元素构成。例如,8的二进制表示为1000,其末尾有3个连续的0,则C[8]包含 2^3=8 个 A[] 数组中的元素,即C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8],这也可从树状数组的示意图中明显观察到。

在包含9个元素的树状数组中,C[i] 与 A[i] 的对应关系如下:
C[1] = A[1]
C[2] = C[1] + A[2] = A[1] + A[2] 
C[3] = A[3]
C[4] = C[2] + C[3] + A[4] = A[1] + A[2] + A[3] + A[4] 
C[5] = A[5]
C[6] = C[5] + A[6] = A[5] + A[6]
C[7] = A[7]
C[8] = C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
C[9] = A[9]

【树状数组的基本操作】
一、每个 C[i] 所包含的数组 A[] 中的元素个数
在编码实践中,每个 C[i] 所包含的数组 A[] 中的元素个数可有下面代码轻松得到。即: 
在定义了lowbit(i)之后,C[i]=A[i-2^k+1]+A[i-2^k+2]+…+A[i],就等价于 A[i−lowbit(i)+1] ~ A[i] 的和。

int lowbit(int i){
    return (-i)&i;    // 返回的值等于上文中的2^k
}
二、直接前驱及直接后继
直接前驱:C[i] 的直接前驱为 C[i-lowbit(i)],即C[i]左侧紧邻的子树的根。
直接后继:C[i] 的直接后继为 C[i+lowbit(i)],即C[i]的父结点。
例如,通过树状数组的示意图,易知C[7]的直接前驱为C[6],C[6]的直接前驱为C[4],C[4]没有直接前驱;
C[5]的直接后继为C[6],C[6]的直接后继为C[8],C[8]没有直接后继。
相应的,C[i]左侧所有子树的根都是C[i]的前驱,C[i]的所有祖先都是C[i]的后继。

三、点更新
若对某个 A[i] 进行修改,如将 A[i] 加上 x,则仅需将 C[i] 及其后继(祖先)都加上 x 便可,而不必对树状数组的所有结点都进行更新。
例如,由于C[5]的后继为C[6]、C[8],所以若将 A[5] 加 2,则仅需将 C[5] 加2、C[6] 加2、C[8] 加2。这通过树状数组的示意图,更容易理解。
树状数组点更新的代码,如下所示:

void update(int i,int val) { //点更新
    while(i<=n) {
        c[i]+=val;
        i+=lowbit(i);  // i的后继(父结点)
    }
}
四、查询前缀和
令 sum(i) 表示 A[] 数组中前 i 元素的前缀和,则 sum(i) 等于 C[i] 加上 C[i] 的前驱。验证如下:
∵ sum(i) = A[1] + A[2] + A[3] + ... + A[i],且有
C[1] = A[1]
C[2] = C[1] + A[2] = A[1] + A[2] 
C[3] = A[3]
C[4] = C[2] + C[3] + A[4] = A[1] + A[2] + A[3] + A[4] 
C[5] = A[5]
C[6] = C[5] + A[6] = A[5] + A[6]
C[7] = A[7]
C[8] = C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
C[9] = A[9]
∴ sum(1) = A[1] = C[1]     → C[1] 没有前驱
sum(2) = A[1] + A[2] = C[2]     → C[2] 没有前驱
sum(3) = A[1] + A[2] + A[3] = C[3] + C[2]     → C[3] 的前驱是C[2]
sum(4) = A[1] + A[2] + A[3] + A[4] = C[4]     → C[4] 没有前驱
sum(5) = A[1] + A[2] + A[3] + A[4] + A[5] = C[5] + C[4]     → C[5] 的前驱是C[4]
sum(6) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] = C[6] + C[4]     → C[6] 的前驱是C[4]

sum(7) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] = C[7] + C[6] + C[4]     → C[7] 的前驱是C[6]、C[4]
sum(8) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8] = C[8] + C[6] + C[4]     → C[8] 没有前驱
sum(9) = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8] + A[9] = C[9] + C[8]     → C[9] 的前驱是C[8]

…… …… ……

树状数组查询前缀和的代码,如下所示:

int preSum(int i) { //前缀和
    int s=0;
    while(i>0) {  // 树状数组的下标从1开始
        s+=c[i];
        i-=lowbit(i);  // i的前驱
    }
    return s;
}
五、查询区间和
若求区间 [i,j] 的和 A[i] + A[i+1] + … + A[j],利用前缀和的思想可得区间 [i,j] 的和值为 preSum(j)-preSum(i-1)。
∵ preSum(j) = A[1] + A[2] + … + A[i-1] + A[i] + … + A[j],
    preSum(i-1) = A[1] + A[2] + … + A[i-1]
∴ preSum(j)-preSum(i-1) = A[i] + A[i+1] + … + A[j],得证。

树状数组查询区间和的代码,如下所示:

int segSum(int i,int j) {
    return preSum(j)-preSum(i-1);
}
六、将 A[x] ~ A[y] 每个元素都加 k
树状数组的经典操作是“单点更新,区间查询”。那么在遇到“洛谷P3368”等“将区间 [x,y] 内每个数加上 k,输出第 x 个数的值”等“区间更新,单点查询”的问题时,怎么办?这就需要利用差分的思想,将“单点更新,区间查询”问题转换为熟悉的“区间更新,单点查询”问题求解。

具体方案为:设原数组为 A[i],定义差分数组 D[i]=A[i]−A[i−1],便可将对数组A[]的“区间更新”操作转化为对差分数组D[]的两次“单点更新”操作。也就是说,此时要将差分数组D[]作为新的原数组构建新的树状数组并实现相关操作。则在新的树状数组中对差分数组D[]的特定“点更新”操作将等效于对原来的原数组A[]所要求进行的“区间更新”操作。要注意,树状数组的下标从1开始,则A[0]空置未用,故有 A[0]=0。
同时,依据差分数组的定义 D[i]=A[i]−A[i−1] 可知,
D[1]=A[1]−A[0]
D[2]=A[2]−A[1]
D[3]=A[3]−A[2]
......
D[i]=A[i]−A[i-1]
上面各式子相加,可得D[1]+D[2]+D[3]+...+D[i]=A[i]-A[0],又由于A[0]=0,所以可得 A[i]=D[1]+D[2]+D[3]+...+D[i] 
显然,利用上文结论 A[i]=D[1]+D[2]+D[3]+...+D[i] ,可将对数组A[]的“单点查询”操作转化为对差分数组D[]的“区间查询”操作。

下面给出一个具体实例,设数组A[]={1,7,3,6,8,5,9,2,10},依据上文所述具体方案,可得差分数组D[]={1,6,-4,3,2,-3,4,-7,8}。假如对数组A[]的区间[2,6]内的每个元素都加上2,则A[]数组变为A[]={1,9,5,8,10,7,9,2,10},差分数组则变为D[]={1,8,-4,3,2,-3,2,-7,8}。

仔细观察,发现“对数组A[]的区间[2,6]内的每个元素都加上2”这个操作执行后,对应的差分数组D[]中只有D[2]、D[7]的值发生改变。原因是,对数组A[]的区间[x,y]内的每个元素都加上k,将会使 A[x] 与前一个元素 A[x-1] 的差增加 k,A[y+1] 与 A[y] 的差减少 k,且  A[x] ~ A[y] 中其他相邻元素间的差值保持不变。所以,对数组A[]的区间[x,y]内的所有元素进行修改,只用修改D[x]与D[y+1]便可:D[x]=D[x]+k,D[y+1]=D[y+1]-k

显然,依据上述方法,便可将对数组A[]的“区间更新”操作转化为对差分数组D[]的两次“单点更新”操作。此操作需要用到树状数组“点更新”操作 update 的代码(https://blog.csdn.net/hnjzsyjyj/article/details/120559543),相关代码内容如下:

int pre=0;
int val;
for(int i=1; i<=n; i++) { //下标从1开始
    scanf("%d",&val);
    update(i,val-pre); //构造差分数组D[]的树状数组
    pre=val;
}
 
update(x,k);
update(y+1,-k);


【参考文献】
https://www.cnblogs.com/pigzhouyb/p/10119601.html
https://www.luogu.com.cn/problem/P2073
https://blog.csdn.net/weixin_30245867/article/details/98500495
https://www.cnblogs.com/RabbitHu/p/BIT.html

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

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

相关文章

操作指南 | 如何参与Moonbeam投票委托

投票委托允许没有时间或者专业度一般的用户能够在治理中拥有话语权。该功能加强了决策流程&#xff0c;并且确保更大范围地代表社区利益。 通过Moonbeam委托平台&#xff0c;你需要 $GLMR 和一个相兼容的钱包。此教程使用MetaMask示范。 如何参与投票委托 前往http://delega…

Python数据容器的总结

1、数据容器分类 数据容器&#xff08;一共五个&#xff09;可以从以下视角进行简单的分类&#xff1a; 是否支持下标索引 支持&#xff1a;列表、元组、字符串&#xff08;序列类型&#xff09; 不支持&#xff1a;集合、字典&#xff08;非序列类型&#xff…

基于SSM的校园代购服务订单管理系统

末尾获取源码 开发语言&#xff1a;Java Java开发工具&#xff1a;JDK1.8 后端框架&#xff1a;SSM 前端&#xff1a;采用Vue技术开发 数据库&#xff1a;MySQL5.7和Navicat管理工具结合 服务器&#xff1a;Tomcat8.5 开发软件&#xff1a;IDEA / Eclipse 是否Maven项目&#x…

日本SolarView Compact光伏发电测量系统 目录遍历漏洞复现(CVE-2023-40924)

日本SolarView Compact光伏发电测量系统 目录遍历漏洞复现CVE-2023-40924 一. 系统简介二.漏洞描述三.影响版本四.fofa查询语句五.漏洞复现六.POC&EXP七、修复建议 免责声明&#xff1a;请勿利用文章内的相关技术从事非法测试&#xff0c;由于传播、利用此文所提供的信息或…

使用java连接Libvirtd

基于springboot web 一、依赖 <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId>&l…

网站优化如何更新文章才有效果,有技巧的更新SEO优化文章

很多新手SEO人员都不太清楚网站为什么要每天更新文章&#xff1f;每天更新文章的主要目的是让新站更快地通过审核期&#xff0c;让老网站避免被降权。同时拥有更多的关键词排名&#xff0c;整体提升网站的流量。 提供新鲜和有用的内容&#xff1a;搜索引擎非常重视新鲜和有用的…

长胜证券:美科技股大跌,甲骨文一夜蒸发超3000亿

当地时间9月12日&#xff0c;美股三大股指跌落&#xff0c;其中&#xff0c;道指跌0.05%&#xff0c;标普指数跌0.57%&#xff0c;纳斯达克指数跌1.04%。本周商场重视通胀与顾客开支等经济数据。当天发布的数据显现&#xff0c;美国8月份线上商品价格创40个月最大降幅。美国房租…

四川Certum便宜多域名SSL证书

多域名SSL证书是一种安全协议&#xff0c;通过SSL/TLS协议对多个域名进行加密传输&#xff0c;实现网站的安全访问和数据传输。它不仅可以保护网站的信息安全&#xff0c;还可以提高网站的信任度和排名 。 而Certum是正规的SSL证书颁发机构&#xff0c;旗下SSL证书产品众多&…

docker 镜像内执行命令显示:You requested GPUs: [0] But your machine only has: []

目录 问题描述&#xff1a; 问题解决&#xff1a; 问题描述&#xff1a; 在docker 镜像环境中&#xff0c;执行“docker exec -it container_name /bin/bash “进入容器之后&#xff0c;执行对应的python命令&#xff0c;显示You requested GPUs: [0] But your machine only…

基于Java+SpringBoot+Vue+uniapp校园点餐小程序(包含协同过滤算法和会员系统,强烈推荐!)

校园点餐小程序 一、前言二、我的优势2.1 自己的网站2.2 自己的小程序&#xff08;小蔡coding&#xff09;2.3 有保障的售后2.4 福利 三、开发环境与技术3.1 MySQL数据库3.2 Vue前端技术3.3 Spring Boot框架3.4 微信小程序 四、功能设计4.1 系统功能结构设计4.2 主要功能描述 五…

【【萌新编写risc-v之软件的学习使用】】

萌新编写risc-v软件的学习使用 对于vscode我其实没什么好说的 就是先配置好环境 其实vscde作为一个编译器的软件 其实并不需要指望能往里面加载多少功能 我们需要做的就是赋予编辑器更好用的功能和体验 有些人会在vscode里面甚至加入波形 仿真 我觉得不如交给 vivado来用 在vs…

车载摄像头进入「多元」增长周期,哪些图像传感器厂商领跑竞争力

随着单车搭载摄像头数量的增加&#xff0c;以及对于摄像头性能要求的提高&#xff0c;汽车级图像传感器&#xff08;CIS&#xff09;市场在2022年成为仅次于手机行业的第二大应用领域。 高工智能汽车研究院监测数据显示&#xff0c;2023年1-6月中国市场&#xff08;不含进出口…

Java基础面试整理

Java基础面试 摘要1. 数据类型和变量2. 控制流语句3. 面向对象编程4. 异常处理5. 集合框架6. 多线程7. 输入输出流8. 设计模式9. 字符串操作10. 数组11. 枚举&#xff08;Enum&#xff09;12. 包&#xff08;Package&#xff09;和导入&#xff08;Import&#xff09;13. 接口&…

飞行动力学 - 第22节-动稳定性与运动方程 之 基础点摘要

飞行动力学 - 第22节-动稳定性与运动方程 之 基础点摘要 1. 稳定性定义2. 动稳定性示意图3. 数值仿真结构框图4. 运动响应类型5. 参考资料 1. 稳定性定义 飞机在平衡状态&#xff08;静止或匀速运动&#xff09;受到扰动&#xff1a; 有回到初始状态的趋势&#xff1b;静稳定…

网页录屏方法推荐,满足你的各种录制需求

“网页可以录屏吗&#xff1f;在浏览器观看了一篇教程&#xff0c;但是不能保存&#xff0c;就想用录屏的方法录制下来&#xff0c;可是不知道网页怎么录屏&#xff0c;家人们帮帮忙&#xff0c;非常感谢&#xff01;” 网页录制是将浏览器中的内容录制成视频的过程&#xff0…

算法分析与设计23.9.12晚第一节

23.9.12晚第一节&#xff1a; 解决问题 问题结构 解决问题思路1&#xff1a;(常规思路) Q0: The simplest case. 先找到最简单的情况。 Q1: Decomposed.(INPUT DS, OUTPUT DS) 观察输入输出的数据结构&#xff0c;尝试分解问题。 Q2: Solution neighborhood. 尝试邻域解的变…

机器学习技术(八)——朴素贝叶斯算法实操

机器学习技术&#xff08;八&#xff09;——朴素贝叶斯算法实操 文章目录 机器学习技术&#xff08;八&#xff09;——朴素贝叶斯算法实操一、引言二、导入相关依赖库三、读取并查看数据四、数据预处理五、构建两种模型进行比较1、只考虑Dayofweek以及District2、加入犯罪时间…

MySQL日常使用记录

1.时间 1.1.时间格式化 yyyy-MM-dd HH:mm:ss格式&#xff0c;如下&#xff1a; select date_format(now(), %Y-%m-%d %H:%i:%s) from dual;date_format函数是将date类型按照指定的格式转换成varchar类型 1.2.日期加减 当前天 1 天 select date_format(now(), %Y-%m-%d), …

【LeetCode-简单题】383. 赎金信

文章目录 题目方法一&#xff1a;数组方法二&#xff1a;哈希表 题目 方法一&#xff1a;数组 先将magazine中的字符转换为26大小的数组中字符的数量然后再遍历ransomNote中的字符&#xff0c;每遍历一个就让字符数量- 1&#xff0c;若字符数量<0 了 &#xff0c;说明magaz…

9.12号

实现一个图形类&#xff08;Shape&#xff09;&#xff0c;包含受保护成员属性&#xff1a;周长、面积&#xff0c; 公共成员函数&#xff1a;特殊成员函数书写 定义一个圆形类&#xff08;Circle&#xff09;&#xff0c;继承自图形类&#xff0c;包含私有属性&#xff1a;半…