本博客源自学长前辈的教解,并参考网络资源。侵权则删,如有错误劳驾指出。问题精深仅一知半解望海涵。
闻名
树状数组:
分析名字: 他是一个类似树形的 数组 ,
作用: 降低复杂度: 频繁对于区间查询和单点修改的操作,或者是单点查询和区间修改的操作。
时间复杂度: 修改和查询都是log(n),相对普通数组修改 o(1) 区间求和是o(n)。
区别线段树: 结构简单(线性结构和树形)代码量少,简洁,能够实现部分线段树的功能。
初见
我们想要实现快速的区间求和。如果用树形,这样的存储方式可能更直观一些:
(x,y)表示的是 数组[x]到数组[y]的和。 这种存储方式叫做线段树。线段树虽然功能多,但总归是树形,难免操作繁多冗长。而树状数组用数组实现了类似树的功能,而且方式巧妙。
线段树的下标编码规则是由上而下、从左至右依次编码。左孩子和右孩子的和 是他们父亲节点的值。然而对于每个右孩子节点的值,我们总能通过其父亲节点值减去左兄弟节点值来计算。可以说 即使没有右节点,也丝毫不影响我们求解对应区间的区间和。这样既能节省这部分空间,又不会将原来的问题复杂化
于是出现了上图所示的树状数组。其中方块中的数字表示对应区间的下标范围,红色字体表示节点的下标,图中的蓝色粗线条将每个节点和其对应的下标连接。但是它的规律还是很特殊的。
>观察上表,我们可以得出如下结论:
一、节点下标为 i 时,节点中对应的最后一个元素下标为 i
二、节点下标对应的二进制数末尾有 k 个 0 ,节点中对应的元素个数为 2 ^ k
三、节点中对应的元素下标是连续的
四、树状数组的节点个数和原数据元素个数相等
相识
核心内容:lowbit —— 巧妙地运用了二进制的 规律 ,进行分层更新:
假设a数组存原始数据,v数组是更新后的树状数组 ;
上半部分的c[i]初始化为c[i]=a[i]。用以描述树状数组的产生过程、
c[i]旁边的红色数字表示的是i的二进制
因为0的性质有些特别,为了避免错误发生我们使用的数组从下标1开始,在简单的了解树状数组后,其实不难发现树状数组就是用来解决 前i个元素和 的这种问题。
求前n个数的和 n n的二进制 v数组的求值过程 (1,1) 1 00001 c1 (1,2) 2 00010 c1+c2 (1,3) 3 00011 c3 (1,4) 4 00100 c2+c3+c4 观察规律可得
求(1,7)= c[7]+c[6]+c[4]=c[0111]+c[0110]+c[0100]由上述表达式可以发现,前7个元素和(1,7)的加数包括 c[0111], 在此基础上,每次将下标从右向左数第一位 1 抹去作为下一个加数的下标, 直到数字变为 0 结束,0111 抹去最后一位 1 得到 0110,0110 抹去最后一位 1 得到 0100,0100 抹去最后一位 1 得到 0000 结束运算。
“抹掉(二进制的)最后一位 1 ”是容易用语言描述的。
体现在代码为:i&(-i)
即:我们得到每次的减数 x =i & ( -i )1 &(-1)补码运算 : 0001 & 1111 = 0001
2 &(-2)补码运算 : 0010 & 1110 = 0010
3 &(-3)补码运算 : 0011 & 1101 = 0001
4 &(-4)补码运算 : 0100 & 1100 = 0100
5 &(-5)补码运算 : 0101 & 1011 = 0001
6 &(-6)补码运算 : 0110 & 1010 = 0010
7 &(-7)补码运算 : 0111 & 1001 = 0001
8 &(-8)补码运算 : 1000 & 1000 = 1000原因:
于正数来说,补码和原码形式是一样的,但对于负数来说,补码便是将原码按位取反后在末位加 1 ,这就导致一个负数绝对值的补码和这个负数的补码在形式上满足:以从右向左数第一个非零位为界,在左侧,负数绝对值的补码和负数的补码各位均不相同,在右侧,负数绝对值的补码和负数的补码各位均为0,将两数做 and 运算得到的数字刚好就是我们需要求的 x ,我们知道 负数的绝对值和负数本身互为相反数,同时也说明一个正数的补码与其相反数的补码做 and 运算 得到的数字就是 x 。
分体解读
lowbit
int lb(int x)
///lowbit 用来求二进制的末尾0的个数 求的是二进制(从右往左数)最后一个1 的位置,
//并且将之前的1全都抹掉,并转为2进制
//i+lb(i)实现了: c[i+lb(i)]刚好是包含c[i]的上一层
{
return x&-x;
}
/*
i= 1 lb(i)=1
i= 2 lb(i)=2
i= 3 lb(i)=1
i= 4 lb(i)=4
i= 5 lb(i)=1
i= 6 lb(i)=2
i= 7 lb(i)=1
i= 8 lb(i)=8
i= 9 lb(i)=1
*/
update
void add(int k, int d, int n)///对某个元素进行加法操作
///元素总个数n ,第k个元素增加d
{
C[k] += d; ///对树的更新
while (k+lb(k) <= n)
C[(k+=lb(k))] += d;// 是+=哦
}
加法操作使得函数可以在 一次循环中 完成赋值和更新
通过i+lowbit(i) 得到i的上一层
所以实际上是从最底层进行向上更新
sum
int sum(int k)///求前n项和:
{
int value = C[k];
while (k > lb(k))
{
k-=lb(k);
value += C[k];
}
return value;
}
int range_sum (int i,int j)///区间求和
{
return sum(j)-sum(i);
}
示例
#include <cstring>
#include <cstdio>
///用于区间求和
int data[200002];
int C[200002];
int lb(int x)///lowbit 用来求二进制的末尾0的个数 求的是二进制最后一个1 的位置
{
return x&-x;
}
int sum(int k)///求前n项和:
{
int value = C[k];
while (k > lb(k))
{
k-=lb(k);
value += C[k];
}
return value;
}
void add(int k, int d, int n)///对某个元素进行加法操作
///元素总个数n ,第k个元素增加d
{
C[k] += d; ///对树的更新
while (k+lb(k) <= n)
C[(k+=lb(k))] += d;
}
int range_sum (int i,int j)///区间求和
{
return sum(j)-sum(i);
}
int main()
{
int cases = 0, n, x, y;//元素总个数n
char buf[10];
while (~scanf("%d",&n) && n)
{
memset(C, 0, sizeof(C));
for (int i = 1; i <= n; ++ i)
{
scanf("%d",&data[i]);
add(i, data[i], n);
}
if (cases ++) puts("");
printf("Case %d:\n",cases);
while (~scanf("%s",buf) && strcmp(buf, "END"))
{
scanf("%d%d",&x,&y);
if (buf[0] == 'S')
{
add(x, y-data[x], n);
data[x] = y;
}
else
{
printf("%d\n",sum(y)-sum(x-1));
}
}
}
return 0;
}
常用扩展
树状数组区间更新
树状数组求第k大
树状数组+离散化
树状数组+并查集
树状数组+搜索