假如需要求前13个元素的前缀和 n u m = 13 = 110 1 2 num=13=1101_2 num=13=11012:
首先处理 b i t = 1 bit=1 bit=1 这一位,其代表的范围是:
[ 110 0 2 + 000 1 2 , 110 0 2 + 000 1 2 ] [1100_2~+0001_2~,~1100_2~+~0001_2] [11002 +00012 , 11002 + 00012]
然后在num上减去他: n u m − = ( 1 < < ( b i t − 1 ) ) = 110 0 2 num−=(1<<(bit−1))=1100_2 num−=(1<<(bit−1))=11002
然后,我们处理 b i t = 3 bit=3 bit=3这一位:其代表的范围是:
[ 100 0 2 + 000 1 2 , 100 0 2 + 010 0 2 ] [1000_2~+~0001_2~,~1000_2~+~0100_2] [10002 + 00012 , 10002 + 01002]
同样,我们在num上减去它
最后我们处理 b i t = 4 bit=4 bit=4 这一位:其代表的范围是:
[ 000 0 2 + 000 1 2 , 000 0 2 + 100 0 2 ] [0000_2~+~0001_2~,~0000_2~+~1000_2] [00002 + 00012 , 00002 + 10002]
我们回顾整个处理流程,可以惊讶的发现,❤️如果我们按照逆序处理,我们每次处理的bit都是当前编号的最后的为1位。
[ c u r − l o w b i t ( c u r ) + 1 , c u r ] [cur−lowbit(cur)+1,cur] [cur−lowbit(cur)+1,cur]
我们将每次处理的bit定义为 lowbit, lowbit(i)即i这个数的最低位是第几位(从右数从1开始)
t r e e [ i ] tree[i] tree[i] 控制
[ i − l o w b i t ( i ) + 1 , i ] [i−lowbit(i)+1,i] [i−lowbit(i)+1,i]
范围内的f[i]
树状数组,将对前 n n n 个元素求和分为了 l o g 2 n log_2n log2n 个不重叠的部分,下图中格内数字代表 t r e e [ i ] tree[i] tree[i]
求和
T tree[maxn];
template
T query(int i){
T res = 0;
while (i > 0){
res += tree[i];
i -= lowbit(i);
}
return res;
}
更新
int n; // BIT 的大小, BIT index 从 1 开始
T tree[maxn];
template
void add(int i, T x){
while (i <= n){
tree[i] += x;
i += lowbit(i);
}
}
lowbit运算
l o w b i t ( x ) = ( x & ( − x ) ) lowbit(x)=(~x~\&~(−x)) lowbit(x)=( x & (−x))
建树
for (i = 1; i <= n; ++i){
scanf("%d", arr + i), update(i, arr[i], c, n);
}
BIT是从下标1开始存储的
307. 区域和检索 - 数组可修改 - 力扣(LeetCode)
二叉搜索树的根节点编号为1,对于每个节点,假如其编号为N,它的左儿子编号为2N,右儿子编号为2N+1。因此,整个二叉搜索树的编号如下
线段树本质也是一个❤️二叉搜索树,区别在于线段树的❤️每一个节点记录的都是一个区间,每个区间都被平均分为2个子区间,作为它的左右儿子。比如说区间[1,10],被分为区间[1,5]作为左儿子,区间[6,10]作为右儿子。❤️当一个区间的左右边界已经相等时,比如[1,1],表示这个区间内只有一个元素了,此时不能再分割,因此它就没有左右儿子节点了
节点 p p p 储存区间 [ l , r ] [~l~,~r~] [ l , r ] 的和,设
m i d = ⌊ l + r 2 ⌋ mid=\lfloor\frac{l+r}{2}\rfloor mid=⌊2l+r⌋
左子树 2 p 2p 2p 存储区间为 [ l , m i d ] [~l~,~mid~] [ l , mid ] , 右子树 2 p + 1 2p~+~1 2p + 1 存储区间为 [ m i d + 1 , r ] [~mid+1~,~r~] [ mid+1 , r ]
左节点对应的区间长度,与右节点相同或者比之恰好多1。
❤️任何区间都是线段树上某些节点的并集,要修改的区间与当前区间存在三种关系:
懒惰标记的初衷就是延迟修改,❤️找到目标区间后直接在相应节点打上懒标记,不需要再向下递归
现在假如我们需要把区间[5,7]每个元素增加2:
注意
找到目标区间后要做两件事:
区间和增加的值要乘区间长度,但懒惰标记的值不用乘,所以懒标记其实是
❤️延迟下放+只下放一层
下传懒惰标记步骤有3步:
这个过程并不是递归的,我们只往下传递一层,以后要用再才继续传递。
// 参数:当前操作左区间,当前操作右区间,当前操作树的节点编号
void build(ll l = 1, ll r = n, ll p = 1)
{
if (l == r) // 到达叶子节点
tree[p] = A[l]; // 用数组中的数据赋值
//到达叶子节点时,左右区间相等,等于要新建的数在原始数组中的下标
else
{
ll mid = (l + r) / 2;
build(l, mid, p * 2); // 先建立左子树
build(mid + 1, r, p * 2 + 1);// 建立右子树
tree[p] = tree[p * 2] + tree[p * 2 + 1]; // 该节点的值等于左右子节点之和
}
}
也可以把参数中当前操作区间的区间端点信息、和存储到struct Node里,就不用以参数传递了
void modifySegment(int l, int r, int value, int num) { // [l,r]每一项都增加value
if (tree[num].l == l && tree[num].r == r) { // 找到当前区间
tree[num].sum += ( r - l + 1 ) * value; // r-l+1是区间元素个数
tree[num].lazy += value;
return;
}
int mid = (tree[num].l + tree[num].r) / 2;
if (r <= mid) { // 在左区间
modifySegment(l, r, value, num * 2);
}
else if (l > mid) { // 在右区间
modifySegment(l, r, value, num * 2 + 1);
}
else { // 分成2块
modifySegment(l, mid, value, num * 2);
modifySegment(mid + 1, r, value, num * 2 + 1);
}
tree[num].sum = tree[num * 2].sum + tree[num * 2 + 1].sum;
}
pushdown函数
void pushdown (int num) {
if(tree[num].l == tree[num].r) { // 叶节点不用下传标记
tree[num].lazy = 0; // 清空当前标记
return;
}
tree[num * 2].lazy += tree[num].lazy; // 下传左儿子的懒惰标记
tree[num * 2 + 1].lazy += tree[num].lazy; // 下传右儿子的懒惰标记
tree[num * 2].sum += (tree[num * 2].r - tree[num * 2].l + 1) * tree[num].lazy; // 更新左儿子的值
tree[num * 2 + 1].sum += (tree[num * 2 + 1].r - tree[num * 2 + 1].l + 1) * tree[num].lazy; // 更新右儿子的值
tree[num].lazy=0; // 清空当前节点的懒惰标记
}
查询
int query (int l, int r, int num) {
if (tree[num].lazy != 0) { // 下传懒惰标记
pushdown(num);
}
if (tree[num].l == l && tree[num].r == r) { // 找到当前区间
return tree[num].sum;
}
int mid = (tree[num].l + tree[num].r) / 2;
if (r <= mid) { // 在左区间
return query(l, r, num * 2);
}
if (l > mid) { // 在右区间
return query(l, r, num * 2 + 1);
}
return query(l, mid, num * 2) + query(mid + 1, r, num * 2 + 1); // 分成2块
}
699. 掉落的方块 - 力扣(LeetCode)
树状数组部分总结自:
树状数组(BIT)—— 一篇就够了 - Last_Whisper - 博客园 (cnblogs.com)
算法学习笔记(2) : 树状数组 - 知乎 (zhihu.com)
(31条消息) 树状数组简单易懂的详解_FlushHip的博客-CSDN博客_树状数组
其中树状数组与线段树区别部分的总结图已无法找到源,应该来自力扣某题解
线段树部分总结自:
(31条消息) 什么是 “线段树” ?_程序员小灰的博客-CSDN博客
算法学习笔记(14): 线段树 - 知乎 (zhihu.com)
线段树详解「汇总级别整理 」 - 掉落的方块 - 力扣(LeetCode)
(33条消息) Balanced Lineup(线段树—指针实现)_AC_Arthur的博客-CSDN博客_指针线段树
一部分代码参考自力扣官方题解