[数据结构]树状数组详解
目录
- 前言
- 为什么是树状数组?
- 极小的常数
- 线性的空间复杂度
- 简短的代码
- 原理详解
- 化一维为树状的储存策略
- 二进制下的质变
- lowbit函数
- 单点修改
- 区间查询
- 例题
- Luogu3374 【模板】树状数组 1
- Luogu3368 【模板】树状数组 2
前言
之前由于树状数组和线段树的修改和查询操作复杂度都是O(log2n)O(log_2n)O(log2n),并且树状数组还只能同时支持单点修改和区间查询(其实骚操作还蛮多,不过个人以为dark不必),无法像线段树那样同时支持区间修改和区间查询。所以博主在高中竞赛期间一直使用线段树代替树状数组,偶尔被卡常就直接照抄一个树状数组模板,没有对这个数据结构进行过深刻的理解和学习,遂于今日还债,争取这个寒假Python学习和竞赛复习两开花好吧。
为什么是树状数组?
上文写的都是为什么不想学树状数组,但既然我开了这个坑,说明树状数组还是有其价值所在,在某些时候有着线段树不具有的优势。
极小的常数
虽然树状数组与线段树的理论时间复杂度都是O(log2n)O(log_2n)O(log2n),但是树状数组的代码由于足够简单、操作较少,在实际运行过程中比线段树更具效率,在相同情况下运行时间往往远低于线段树,在各种卡常数场景下极具应用价值。
线性的空间复杂度
这也是树状数组相比线段树独特的优势所在,由于树状数组空间复杂度仅为O(n)O(n)O(n),仅占用了线性的空间,所以被广泛应用在嵌套类的数据结构中。此类数据结构由于空间复杂度的叠加往往会遭遇空间不足的窘境,此时线性复杂度的树状数组就成为了嵌套的首选。
简短的代码
树状数组的原理非常简洁(但不一定好理解),虽然功能也相应的比较简单,但是代码实现也变得非常容易。即使没有搞清楚树状数组的原理,直接硬背模板也不失为一个选择。同时对于已经理解了的选手,实现树状数组也比实现线段树要更快。
原理详解
化一维为树状的储存策略
树状数组的独特在于人为地将一维的数组排布为树状,并让一个节点储存更多的信息:
以大小为888的树状数组为例:1,3,5,71,3,5,71,3,5,7这四个节点只储存了自己对应位置的数组的信息;222号节点储存了1,21,21,2两个位置的信息,666号节点储存了5,65,65,6两个位置的信息;444号节点储存了1,2,3,41,2,3,41,2,3,4四个位置的信息;888号节点则储存了1∼81\sim 81∼8所有位置的信息。
到此为止,我们可以观察出树状数组的信息储存策略:
单个节点储存的位置信息数量等于于节点编号的最大的、是二的次幂的约数。如666的约数有1,2,3,61,2,3,61,2,3,6,其中是二的次幂的只有222,所以666号节点储存了两个位置的信息;对于888号节点,它的约数有1,2,4,81,2,4,81,2,4,8,这些都是二的次幂,其中888是最大的,所以888号节点储存888个位置的信息。
每个节点只会储存编号小于等于自己编号的位置的信息。
每个节点储存信息的位置编号是连续的。
根据以上三点,我们就可以求出任意编号的节点应该储存哪些位置的信息,即每个节点应该储存包括自己对应的位置在内的、编号小于自己且连续的、数量为自己节点编号的最大二次幂约数的位置的信息。
但是仅仅凭借这个规则,我们并不能快速求出每个节点的储存范围,而如果修改了一个位置的信息,该向上更新哪些节点也并不好求出,所以我们需要更进一步的挖掘树状数组储存策略的内涵。
二进制下的质变
lowbit函数
如果将上述规则放在二进制下解读,这个规则或许没有那么复杂:
可以发现,每个节点储存的位置信息数等于222的节点编号末尾连续000的个数次幂,如110110110末尾有111个000,那么666号节点就储存了212^121即222个位置的信息。
再结合第二、三条规则,可以发现对于一个二进制的编号为A100⋯0⏟n个0,n≥0A1\underbrace{00\cdots0}_{n个0,n\ge0}A1n个0,n≥000⋯0(AAA为任意010101串)的节点,它会储存从编号再[A00⋯0⏟n个01,A100⋯0⏟n个0][A\underbrace{00\cdots0}_{n个0}1, A1\underbrace{00\cdots0}_{n个0}][An个000⋯01,A1n个000⋯0]区间的所有位置的信息。比如101010号节点(二进制下为101010101010,AAA部分为前两位101010,同时以101010结尾)就会储存编号在1001∼10101001\sim 10101001∼1010(即9∼109\sim 109∼10)的位置信息。
所以,问题的关键在于快速求出一个数二进制下末尾的100⋯0100\cdots 0100⋯0串对应的数值。
在计算机里,通过二进制位运算i&(−i)i \& (-i)i&(−i)就可以O(1)O(1)O(1)求出对应节点记录位置信息的数量。这个操作得以实现是基于计算机内对负数按位取反再加一的储存方式。
对于一个二进制正整数A100⋯0⏟n个0,n≥0A1\underbrace{00\cdots0}_{n个0,n\ge0}A1n个0,n≥000⋯0,其在计算机内的存储为:
0A100⋯0⏟n个00\ A1\underbrace{00\cdots0}_{n个0}0 A1n个000⋯0(首位的000表示这是一个正数)
而它的相反数则是先对0A100⋯0⏟n个00\ A1\underbrace{00\cdots0}_{n个0}0 A1n个000⋯0按位取反得到:
1inv(A)011⋯1⏟n个11\ inv(A)0\underbrace{11\cdots1}_{n个1}1 inv(A)0n个111⋯1(inv(A)inv(A)inv(A)表示010101串AAA按位取反后得到的串)
再加上111得到:
1inv(A)100⋯0⏟n个01\ inv(A)1\underbrace{00\cdots0}_{n个0}1 inv(A)1n个000⋯0
不难发现,一个数和它的相反数在计算机中储存时恰好只有我们要求的那部分末尾是相同的,而前面的部分每一位都是取反关系。所以,只需要将一个数和它的相反数“&\&&”起来,就能得到我们想要求的数值。这个函数有个特有的名称,叫做lowbitlowbitlowbit函数,即:
lowbit(x)=x&(−x)lowbit(x)=x\&(-x)lowbit(x)=x&(−x)
单点修改
从上一部分我们可以看到,编号为A100⋯0⏟n个0,n≥0A1\underbrace{00\cdots0}_{n个0,n\ge0}A1n个0,n≥000⋯0的节点,会记录A00⋯0⏟n个01∼A100⋯0⏟n个0A\underbrace{00\cdots0}_{n个0}1\sim A1\underbrace{00\cdots0}_{n个0}An个000⋯01∼A1n个000⋯0之间所有位置的数组的信息。同样的,当我们修改了某一个点的信息,就需要上溯每一个储存了该位置信息的节点,做出相应修改。
我们先尝试找到某个节点上溯的第一个节点:
按照上述结论反推,对于一个编号二进制为A01BA01BA01B(AAA为任意010101串,BBB为形如11⋯1⏟n个1,n≥000⋯0⏟m个0,m≥0\underbrace{11\cdots 1}_{n个1,n\ge0}\ \underbrace{00\cdots 0}_{m个0,m\ge 0}n个1,n≥011⋯1 m个0,m≥000⋯0的010101串)的节点,包含了该节点信息的节点的编号一定大于A01BA01BA01B,且编号的二进制末尾一定为100⋯0⏟p个0,p≥n+m+11\underbrace{00\cdots 0}_{p个0,p\ge n+m+1}1p个0,p≥n+m+100⋯0(因为末尾要能够大于01B01B01B这个串)。而且上溯的第一个节点编号一定是满足这些条件的编号中最小的一个,所以对应的节点编号显然为A100⋯0⏟n+m+1个0A1\underbrace{00\cdots 0}_{n+m+1个0}A1n+m+1个000⋯0,同时可以观察到:
A100⋯0⏟n+m+1个0=A01B+100⋯0⏟m个0=A01B+lowbit(A01B)A1\underbrace{00\cdots 0}_{n+m+1个0}=A01B+1\underbrace{00\cdots0}_{m个0}=A01B+lowbit(A01B)A1n+m+1个000⋯0=A01B+1m个000⋯0=A01B+lowbit(A01B)
原来,编号为vvv的节点上溯的第一个节点的编号就是节点自己的编号加上lowbitlowbitlowbit值,即v+lowbit(v)v+lowbit(v)v+lowbit(v),那么只要不断地加上当前节点的lowbitlowbitlowbit值,就可以不断上溯,完成更新操作。
所以我们可以得到下面的执行单点修改函数(例子中为给位置为vvv的数加上Δ\DeltaΔ):
void add(int v,int delta){for(;v<=n;v+=lb(v))num[v]+=delta;}
由于每次上溯后,编号的二进制位末尾的100⋯0100\cdots 0100⋯0串中000的个数都会至少增加一个,在log2nlog_2nlog2n次运算之内,该节点编号就会大于nnn,所以该操作的时间复杂度为O(log2n)O(log_2 n)O(log2n)。
区间查询
如果我们想要完成对区间[1,v][1,v][1,v]的查询,最简单粗暴的办法就是用for循环将1∼v1\sim v1∼v中的所有位置都遍历一遍,但是很显然这样没有利用上我们辛辛苦苦搭建起来的树状数组,考虑怎样利用上那些记录了多个位置信息的节点。
根据树状数组的存储策略,编号为A100⋯0⏟n个0,n≥0A1\underbrace{00\cdots0}_{n个0,n\ge 0}A1n个0,n≥000⋯0的节点,会记录A00⋯0⏟n个01∼A100⋯0⏟n个0A\underbrace{00\cdots0}_{n个0}1\sim A1\underbrace{00\cdots0}_{n个0}An个000⋯01∼A1n个000⋯0之间所有位置的数组的信息。所以我们只需要A100⋯0⏟n个0A1\underbrace{00\cdots0}_{n个0}A1n个000⋯0这一个节点就可以得到[A00⋯0⏟n个01,A100⋯0⏟n个0][A\underbrace{00\cdots0}_{n个0}1,A1\underbrace{00\cdots0}_{n个0}][An个000⋯01,A1n个000⋯0]这个区间里数组的信息,那么只要找到[1,v][1,v][1,v]区间中储存了多个位置的数组信息且不重复的节点,将它们储存的信息整合在一起,就能得到最终结果。
假设我们要查询的区间为[1,A100⋯0⏟n个0,n≥0100⋯0⏟m个0,m≥0][1,A1\underbrace{00\cdots0}_{n个0,n\ge 0}1\underbrace{00\cdots0}_{m个0,m\ge0}][1,A1n个0,n≥000⋯01m个0,m≥000⋯0],首先这个节点本身就会储存[A100⋯0⏟n+m个01,A100⋯0⏟n个0100⋯0⏟m个0][A1\underbrace{00\cdots0}_{n+m个0}1,A1\underbrace{00\cdots0}_{n个0}1\underbrace{00\cdots0}_{m个0}][A1n+m个000⋯01,A1n个000⋯01m个000⋯0]这个区间的数组信息,所以问题就转换为求[1,A100⋯0⏟n+m+1个0][1,A1\underbrace{00\cdots0}_{n+m+1个0}][1,A1n+m+1个000⋯0]这个区间内的信息,我们成功的将问题的范围从A100⋯0⏟n个0100⋯0⏟m个0A1\underbrace{00\cdots0}_{n个0}1\underbrace{00\cdots0}_{m个0}A1n个000⋯01m个000⋯0缩小到了A100⋯0⏟n+m+1个0A1\underbrace{00\cdots0}_{n+m+1个0}A1n+m+1个000⋯0。同时,不难发现:
A100⋯0⏟n+m+1个0=A100⋯0⏟n个0100⋯0⏟m个0−lowbit(A100⋯0⏟n个0100⋯0⏟m个0)A1\underbrace{00\cdots0}_{n+m+1个0}=A1\underbrace{00\cdots0}_{n个0}1\underbrace{00\cdots0}_{m个0}-lowbit(A1\underbrace{00\cdots0}_{n个0}1\underbrace{00\cdots0}_{m个0})A1n+m+1个000⋯0=A1n个000⋯01m个000⋯0−lowbit(A1n个000⋯01m个000⋯0)
因此,只需要不断地减去当前查询区间右端点的lowbitlowbitlowbit值,就能快速地缩小查询范围,并从右端点对应地节点直接获取部分信息,最终组成答案,完成查询操作。
所以我们得到了下面的区间查询函数(例子中为查询区间和):
int ask(int v){int re=0;for(;v;v-=lb(v))re+=num[v];return re;}
由于每次减去自身的lowbitlowbitlowbit后,右端点的二进制都会少掉最右端的111,在log2nlog_2nlog2n次运算之内右端点就会变成000,所以树状数组可以以O(log2n)O(log_2n)O(log2n)的复杂度完成对任意以111为左端点的区间[1,v][1,v][1,v]的查询,两次查询的结果相减就可以求出任意区间。
例题
Luogu3374 【模板】树状数组 1
原题链接:https://www.luogu.com.cn/problem/P3374
单点修改+区间查询,对应上文讲解中使用的例子,这里直接给出代码:
#include<bits/stdc++.h>
#define lb(i) (i&(-i))
using namespace std;
const int M=5e5+5;
int n,m,num[M];
void add(int v,int delta){for(;v<=n;v+=lb(v))num[v]+=delta;}
int ask(int v){int re=0;for(;v;v-=lb(v))re+=num[v];return re;}
void in()
{scanf("%d%d",&n,&m);for(int i=1,a;i<=n;++i)scanf("%d",&a),add(i,a);
}
void ac()
{for(int i=1,a,b,c;i<=m;++i){scanf("%d%d%d",&a,&b,&c);if(a-1)printf("%d\n",ask(c)-ask(b-1));else add(b,c);}
}
int main()
{in(),ac();system("pause");
}
Luogu3368 【模板】树状数组 2
原题链接:https://www.luogu.com.cn/problem/P3368
区间修改+单点查询,通过差分就可以转换成单点修改+区间查询。
差分就是将原数列{ai}\{a_i\}{ai}修改为原来的数与前一个位置的数之差bi=ai−ai−1b_i=a_i-a_{i-1}bi=ai−ai−1得到数列{bi}\{b_i\}{bi},这样新数列的前缀和就等于原数列对应位置的数,即:
∑i=1kbi=ak\sum_{i=1}^{k}b_i=a_ki=1∑kbi=ak
当我们想要对区间[x,y][x,y][x,y]中的每一个数都加上Δ\DeltaΔ时,只需要对差分数列的第xxx项加上Δ\DeltaΔ,第y+1y+1y+1项减去Δ\DeltaΔ就完成了操作。
代码如下:
#include<bits/stdc++.h>
#define lb(i) (i&(-i))
using namespace std;
const int M=5e5+5;
int n,m,num[M];
void add(int v,int delta){for(;v<=n;v+=lb(v))num[v]+=delta;}
int ask(int v){int re=0;for(;v;v-=lb(v))re+=num[v];return re;}
void in()
{scanf("%d%d",&n,&m);for(int i=1,a=0,b;i<=n;++i)scanf("%d",&b),add(i,b-a),a=b;
}
void ac()
{for(int i=1,a,b,c,d;i<=m;++i){scanf("%d%d",&a,&b);if(a-1)printf("%d\n",ask(b));else{scanf("%d%d",&c,&d);add(b,d),add(c+1,-d);}}
}
int main()
{in(),ac();system("pause");
}
[数据结构]树状数组详解相关推荐
- 【数据结构】树状数组详解(Leetcode.315)
前言 最近做题时遇到一个关于树状数组的题力扣https://leetcode-cn.com/problems/count-of-smaller-numbers-after-self/但是CSDN上仅有 ...
- 树状数组详解(附图解,模板及经典例题分析)
导言 深藏于算法与数据结构中的思想非常的美妙,尤其是当我们一个一个攻克其中的难点,体会其中蕴含的"哲理"时, A 题的自信力也会有所增加,心情也会格外的舒爽.最近重新接触了树状数组 ...
- AcWing 241 楼兰图腾(树状数组详解)
树状数组 问题引入 树状数组是一种实现起来比较简单的高级数据结构. 我们知道,对于一个数组a[i],其前缀和s[i]表示a数组里面前i个元素之和,而求区间l到r的元素之和可以用s[r] - s[l-1 ...
- szu 寒训第二天 树状数组 二维树状数组详解,以及树状数组扩展应用【求逆序对,以及动态第k小数】
树状数组(Binary Index Tree) 树状数组可以解决可以转化为前缀和问题的问题 这是一类用以解决动态前缀和的问题 (有点像线段树简版) 1.对于 a1 + a2 + a3 + - + an ...
- 树状数组详解(超详细)(完整代码在四 五最后)
一,树状数组的优点 前缀和的思想,可以通过O(n)的预处理,使得多次查询区间值都是o(1),但只能解决不修改,多次查询的问题. 差分思想,能通过差分数组,将区间修改变成O(1)的,最后通过一次O(n) ...
- 数据结构--树状数组
文章目录 1. 树状数组 2. 单点修改 3. 区间修改 4. 完整代码 5. 参考文献 1. 树状数组 类似数据结构:线段树(Segment Tree) 树状数组 跟 线段树 的区别: 树状数组能做 ...
- 数据结构--树链剖分详解
数据结构--树链剖分详解 关于模板题---->传送门 题目描述 如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作1: 格式: 1 x y z 表示将 ...
- js 数组 实现 完全树_算法和数据结构 | 树状数组(Binary Indexed Tree)
本文来源于力扣圈子,作者:胡小旭.点击查看原文 力扣leetcode-cn.com 树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为 Fenwick 树.其初 ...
- JavaScript数据结构与算法——数组详解(下)
1.二维与多维数组 JavaScript只支持一维数组,但是通过在数组里保存数组元素的方式,可以轻松创建多维数组. 1.1 创建二维数组 二维数组类似一种由行和列构成的数组表格,在JavaScript ...
- 数据结构——树状数组
我们今天来讲一个应用比较广泛的数据结构--树状数组 它可以在O(nlogn)的复杂度下进行单点修改区间查询,下面我会分成三个模块对树状数组进行详细的解说,分别是树状数组基本操作.树状数组区间修改单点查 ...
最新文章
- csv文件与字典,列表等之间的转换小结【Python】
- Practical Common Lisp
- python【数据结构与算法】内建模块itertools(操作迭代对象)
- 站长们都会,但是都会写错的robots!
- linux 远程控制权限,总结一下linux远程控制方法
- pomelo php,Nginx 502 Bad Gateway 自动重启shell脚本
- 神策 FM | CEO 荐书—《斯坦福商业决策课》
- 工业以太网在工业领域的应用特点详解
- 深入理解Azure自动扩展集VMSS(3)
- javaweb文件压缩下载
- 通过代理下载Google Code
- TI am3352 gpio 驱动
- 一文带你玩转 DataStore
- Isilon旧机器重新初始化
- VBA实现 Excel自动填充
- 利用LabVIEW开发应变量测试
- 芬兰Vaisala温湿度变送器HMT330
- 逆向爬虫08 并发异步编程
- BMS(电池管理系统)第五课 ——核心!!!SOH算法开发
- python基础之写文件操作