树状数组:
1.单点修改,区间求和
1.快速求前缀和 o(logn)
2.修改某一个数 o(logn)
2.拓展-区间修改,求单点和(结合差分)
3.拓展-区间修改,区间求和
lowbit函数用于取x二进制最低位的1与后面的0组成的数字
原理:二进制的负数是正数取反加一
int lowbit(int x)
{
return t & (-t);
}
原理
假设x = 2ik + 2ik-1 +…+ 2i1
ik >= ik-1 >= … i1
将区间划分成(左开右闭):
(x - 2i1, x]
包含2i1个数,2i1是右边界二进制表示的最后一位1与后面的0构成的数
(x - 2i1 - 2i2, x - 2i1]
包含2i2个数,2i2是右边界二进制表示的最后一位1与后面的0构成的数
(x - 2i1 - 2i2 - 2i3, x - 2i1 - 2i2] …
…
(0, x - 2i1 - 2i2 -…- 2ik-1)
包含2ik个数,包含2ik个数,2ik是右边界二进制表示的最后一位1与后面的0构成的数
-> 对于其中任意区间 (L,R] 长度是R的二进制表示的最后一位1所对应的数
-> 则对于其中的每一个区间,可以表示为 [R - lowbit(R) + 1, R] (左闭右闭)
-> 可以用数组c[R]表示这个区间总和
-> c[x - lowbit(x) + 1, x] 表示以x为右端点长度为lowbit(x)+1的区间的所有数的和
-> 对于1~x所有数的和,最多可以拆成logx个c[x]的和
树如图所示
---------------------------------------------------------------------c16
------------------------------------c8
----------------c4 -------------c12
------c2 ------c6 -----c10 -----c14
-c1 -c3 -c5 -c7 -c9 -c11 -c13 -c15
a 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
c16 = a16 + c15 + c14 + c12 + c8
10000 01111 01110 01100 01000
tr[i]表示,以i结尾,长度是lowbit(i)的区间内元素的和!!!
求x的子节点
先将x减去1(eg 10000变成01111),每个1对应一个儿子(eg 01111 01110 01100 01000)
c[x] = a[x] + c[x-1] + c[x-1-lowbit(x-1)] + c[x-1-lowbit(x-1)-lowbit(x-2)] +…+ 0
对于x-1-lowbit(x-1)表示将x-1的最后一个一去掉,即实现01111 -> 01110
===========================
代码实现
1.通过父节点找子节点
对于x-1,有k个儿子,即循环k次,每次减掉最后的1即找到每一个儿子( eg求前缀和)
int sum(int x){
int res = 0;
for ( int i = x; i; i -= lowbit(i) ) tr[x] += tr[i];
return res;
}
2.通过子节点找父节点
更改x的值,即更改(上图)包含x的所区间
假如x 0011100 -> x父节点p 0100000(唯一的) 0011100 + 0000100 = 0100000
-> p = x + lowbit(x) -> p2 = p + lowbit§ -> p3 = … -> 直到等于n
最多持续logx次(eg某个数加c)
void add(int x, int c){
for ( int i = x; i <= n; i += lowbit(i) ) tr[i] += c;
}
3.初始化树状数组
for ( int i = 1; i <= n; i ++ ) add(i, a[i])
->时间复杂度o(nlogn)
========================================================
例题
1.简单的整数问题l
区间修改,单点求值
题目
给定长度为 N 的数列 A,然后输入 M 行操作指令。
C l r d,表示把数列中第 l∼r 个数都加 d
Q x,表示询问数列中第 x 个数的值
对于每个询问,输出一个整数表示答案。
思路
树状数组的拓展
将某一区间的每个数加上c,求a[x] -> 差分
将问题变成单点修改,区间求和
树状数组中存储a[i]的差分
构造差分:
b[i] = a[i] - a[i - 1]
代码
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
int a[N], tr[N], n, m;
int lowbit(int x)
{
return x & -x;
}
void add(int x, int c)
{
for ( int i = x; i <= n; i += lowbit(i) ) tr[i] += c;
}
LL sum(int x)
{
LL res = 0;
for ( int i = x; i; i -= lowbit(i) ) res += tr[i];
return res;
}
int main()
{
cin >> n >> m;
for ( int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for ( int i = 1; i <= n; i ++ ) add(i, a[i] - a[i - 1]); //树状数组中存a[i]的差分
while ( m -- )
{
char op[2];
scanf("%s", op);
if ( *op == 'C' )
{
int l, r, d;
scanf("%d%d%d", &l, &r, &d);
add(l, d), add(r + 1, -d); //差分日常操作
}
else
{
int x;
scanf("%d",&x);
printf("%lld\n", sum(x)); //差分数组的前缀和便是a[i]
}
}
return 0;
}
~~~~~~~~~~~~~~~~~
2.简单的整数问题ll
(树状数组+差分+巧妙运算)区间修改,区间求和**
题目
给定长度为 N 的数列 A,然后输入 M 行操作指令
C l r d,表示把数列中第 l∼r 个数都加 d
Q l r,表示询问区间 [l, r] 内元素的和
对于每个询问,输出一个整数表示答案。
思路
构造a数组的差分数组b
区间求和:求a1 + a2 +…+ ax (ax = b1 + b2 +…+ bx)
//朴素法(时间复杂度极高)
for ( int i = 1; i <= x; i ++ )
for ( int j =1; j <= i; j ++ )
res += b[j];
//优化法
1.a1到ax的和如图1)所示,要求图1)的和,先将图1)补成图2)
2)
1) b1 b2 b3 b4 ... bx 注:在没有b的第0行也要补,补完后是x+1行
b1 b1 b2 b3 b4 ... bx
b1 b2 b1 b2 b3 b4 ... bx
b1 b2 b3 -> b1 b2 b3 b4 ... bx
... ...
b1 b2 b3 b4 ... bx b1 b2 b3 b4 ... bx
2)的求和:(b1 + b2 + ... + bx) * (x + 1)
1)的求和:(b1 + b2 + ... + bx) * (x + 1) - (b1 + 2*b2 + ... + x*bx)
-> 1)的和即为 2)减去i*b的前缀和
-> 用tr1维护bi的前缀和,tr2维护i*bi的前缀和
-> sum(tr1, x) * (x + 1) - sum(tr2, x)
代码
#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 1e5 + 10;
LL tr1[N], tr2[N];
int a[N], n, m;
int lowbit(int x)
{
return x & -x;
}
void add(LL tr[], int x, LL c)
{
for ( int i = x; i <= n; i += lowbit(i) ) tr[i] += c;
}
LL sum(LL tr[], int x)
{
LL res = 0;
for ( int i = x; i; i -= lowbit(i) ) res += tr[i];
return res;
}
LL prefix_sum(int x)
{
return sum(tr1, x) * (x + 1) - sum(tr2, x);
}
int main()
{
cin >> n >> m;
for ( int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);
for ( int i = 1; i <= n; i ++ )
{
add(tr1, i, a[i] - a[i - 1]);
add(tr2, i, (LL)(a[i] - a[i - 1]) * i); //可能会爆int
}
while ( m -- )
{
char op[2];
int l, r, d;
scanf("%s%d%d", op, &l, &r);
if ( *op == 'C' )
{
scanf("%d", &d);
add(tr1, l, d), add(tr2, l, l * d);
add(tr1, r + 1, -d), add(tr2, r + 1, (r + 1) * -d);
}
else printf("%lld\n", prefix_sum(r) - prefix_sum(l - 1));
}
return 0;
}