树状数组
树状数组
1.对树状数组的基本了解
树状数组,顾名思义就是用数组来模拟树形结构,主要用来解决大部分基于区间上的更新以及求和问题
优点是修改和查询的复杂度都是\(O(logN)\),相比线段树系数要少很多,比传统数组要快,并且容易写
缺点是遇到复杂的区间问题还是不能解决,功能有限
2.区间查询,单点更新
P3374 【模板】树状数组 1 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
已知一个正整数可以用二进制来表示
比如 \(7(111)_2 = 2^2(100)_2 + 2^1(010)_2 + 2^0(001)_2\)
那么我们可以设 \(x = 2^{i_k} + 2^{i_{k-1}} + 2^{i_{k-2}} + \cdots + 2^{i_2} + 2^{i_1}\)
我们将 0~x 区间进行分段,如下:
\((x-2^{i_1},\quad x] \quad\quad len = 2^{i_1}\)
\((x-2^{i_1}-2^{i_2},\quad x-2^{i_1}] \quad\quad len = 2^{i_2}\)
\((x-2^{i_1}-2^{i_2}-2^{i_3},\quad x-2^{i_1}-2^{i_2}] \quad\quad len = 2^{i_3}\)
\(\cdots\)
\((0,\quad x-2^{i_1}-2^{i_2}-\cdots - 2^{i_k}] \quad\quad len = 2^{i_k}\)
我们将每段设为 \((L, R]\) ,以上可知,\(L = R - len\)$。而 $$len$$ 正好等于 $$lowbit(R)$
所以每段为 \((R - lowbit(R), R]\),左边加1,使开区间变为闭区间,即
所以可知 (设 s[x] 为 1~x 的区间和)
一直加到 c[0] 为止,c[0] 不存在
写成代码如下
int getsum(int x){
int sum = 0;
while (x > 0){
sum += c[x];
x -= lowbit(x);
}
return sum;
}
对于c[R]树状数组,R=1~8来说,作图如下:

从图中可知,如果改变了 c[1] 的值,那么包含它的也要改变
如下:
c[1] --> c[2] --> c[4] --> c[8] \(1(0001)_2 --> 2(0010)_2 --> 4(0100)_2 --> 8(1000)_2\)
c[3] --> c[4] --> c[8] \(3(0011)_2 --> 4(0100) --> 8(1000)_2\)
从上面两个过程中我们可以看出,下标变化的规律是加上本身的\(lowbit()\)
即如果改变的值的下标是 i,那么之后要改变的值的下标应该是这样的
\(1 --> 1+lowbit(1) --> 1+lowbit(1)+lowbit(1+lowbit()) --> \cdots\)
一直加到 n(数组的长度) 为止
写成代码如下
void update(int i, int k){
while (i <= n){
c[i] += k;
i += lowbit(i);
}
}
以上,我们就学会了树状数组的区间查询和单点更新
P2880 [USACO07JAN] Balanced Lineup G - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
3.单点查询,区间更新
P3368 【模板】树状数组 2 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
如果题目要求是把 x~y 区间内的所有值全部加上 k 或者减去 k,然后查询某个点的值,这种时候应该怎么做呢
如果是像上面的树状数组来说,就必须把 x-y 区间内每个值都更新,这样的复杂度肯定是不行的,这个时候,就不能再用数据的值建树了
这里我们引入差分,利用差分建树
对于差分来说,会有一个差分数组 d 来储存原数组的差值
例如
\(a[] = 1 \quad 2 \quad 3 \quad 5 \quad 6 \quad 9\)
\(d[] = 1 \quad 1 \quad 1 \quad 2 \quad 1 \quad 3\)
如果我们把[2,5]区间内值加上2,则变成了
\(a[] = 1 \quad 4 \quad 5 \quad 7 \quad 8 \quad 9\)
\(d[] = 1 \quad 3 \quad 1 \quad 2 \quad 1 \quad 1\)
当某个区间[x,y]值改变了,区间内的差值是不变的,只有D[x]和D[y+1]的值发生改变
我们只需要更改两个值,就可以更新一个区间的值了
代码如下
int lowbit(int x){
return x & -x;
}
void update(int i, int k){
while (i <= n){
c[i] += k;
i += lowbit(i);
}
}
int getsum(int x){
int sum = 0;
while (x > 0){
sum += c[x];
x -= lowbit(x);
}
return sum;
}
void solve(){
//初始化
for (int i = 1; i <= n; i++){
update(i, a[i] - a[i - 1]);
}
//更新
update(x, k);
update(y + 1, -k);
//查询i位置的值
int sum = getsum(i);
}
与区间查询,单点更新的区别就是初始化以及更新方式
P5057 [CQOI2006]简单题 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
4.区间更新,区间查询
给出一个数列 ,你需要完成如下操作:
-
\(1 \quad x \quad y \quad k\) :表示将区间\([x, y]\)内每个数都加上 k ;
-
\(2 \quad x \quad y\) :表示输出区间\([x, y]\)内每个数的和。
上面我们说的差值建树状数组,得到的是某个点的值,那如果我既要区间更新,又要区间查询怎么办
这里我们还是利用差分,由上面可知
已知差分数组,求和\(sum=\sum_{i=1}^n{a[i]} = \sum_{i=1}^n\sum_{j=1}^i{d[j]}\)
即
\(a[1]+a[2]+a[3]+\cdots+a[n]\)
\(=d[1] + (d[1]+d[2]) + \cdots + (d[1] + d[2] + \cdots + d[n])\)
\(=n*d[1] + (n - 1)*d[2] + \cdots + d[n]\)
\(=n*(d[1] + d[2] + \cdots + d[n]) - (0*d[1] + 1*d[2] + \cdots + (n - 1)*d[n])\)
所以上式变为\(sum=\sum_{i=1}^n{a[i]} = n*\sum_{i=1}^nd[i] - \sum_{i=1}^n(d[i]*(i-1))\)
因此我们要维护两个树状数组,即用 d[i] 和 d[i]*(i-1) 来建树状数组
代码如下
int lowbit(int x){
return x & -x;
}
void update(int i, int k){
int x = i;
while (i <= n){//维护两个树状数组,c1放的是d[i],c2放的是d[i]*(i-1)
c1[i] += k;
c2[i] += k * (x - 1);
i += lowbit(i);
}
}
int getsum(int x){
int sum = 0, i = x;
while (x > 0){
sum += i * c1[x] - c2[x];
x -= lowbit(x);
}
return sum;
}
void solve(){
//初始化
for (int i = 1; i <= n; i++){
update(i, a[i] - a[i - 1]);
}
//更新
update(x, k);
update(y + 1, -k);
//求[x, y]的区间和
int sum = getsum(y) - geysum(x - 1);
}
5.拓展-二维树状数组
5.1单点更新,区间查询
给出一个 n×m 的零矩阵 A ,你需要完成如下操作:
-
\(1 \quad x \quad y \quad k\) :表示元素 \(A_{x,y}\) 自增 k ;
-
\(2 \quad a \quad b \quad c \quad d\) :表示询问左上角为 (a,b) ,右下角为 (c,d) 的子矩阵内所有数的和。
我们在进行单点操作的时候,考虑的是修改这个位置后有多少位置会被影响到。显然,我们可以通过i += lowbit(i)的方式,逆推原本位置被树状数组所管理的区间。对于二维树状数组来说,我们实际上进行的也是一模一样的操作。但是这次不同,如果一维树状数组维护的是一个一维的数列,那么二维树状数组就是维护了一个一个二维的矩阵了。
我们现在考虑的是这个位置修改后,会影响到几个矩阵内的值。当然,上面已经提到过,二维树状数组其实是一个树状数组套树状数组,也就是每个节点都是一个树状数组,因此,我们在进行修改操作的时候,对于第一维的树状数组,我们要考虑修改这个位置的树状数组会影响到多少个位置的树状数组,进入到第二维时考虑的就是这个位置修改后会影响到多少个位置。可以发现,这样的操作后其实就是修改了一个一个的小矩形了
void update(int x, int y, int k) {
while (x <= n){
while (y <= m){
c[x][y] += k;
y += lowbit(y);
}
x += lowbit(x);
}
}
我们已经提到过,单点修改操作的本质其实是跟一维树状数组是一样的,那么查询操作也是同理,依然是考虑有多少个小矩形要加。当然,这次查询的,就是二维前缀和了
int getsum(int x, int y) {
int sum = 0;
while (x > 0){
while (y > 0){
sum += c[x][y];
y -= lowbit(x);
}
x -= lowbit(y);
}
return sum;
}
我们知道,求二维前缀和的公式为\(ans = s_{x_2,y_2}-s_{x_1-1,y_2}-s_{x_2,y_1-1}+s_{x_1-1,y_1-1}\)
所以我们求区间和\((x1, y1)\) ~ \((x2, y2)\)的时候,只需要求
getsum(x2, y2) - getsum(x1 - 1, y2) - getsum(x2, y1 -1) + getsum(x1 - 1, y1 - 1)
5.2区间更新,单点查询
给出一个 n×m 的零矩阵 A ,你需要完成如下操作:
-
\(1 \quad a \quad b \quad c \quad d \quad k\) :表示询问左上角为 (a,b) ,右下角为 (c,d) 的子矩阵内所有数都自增加 k;
-
\(2 \quad x \quad y\) :表示询问元素 \(A_{x,y}\) 的值。
现在我们的需求又变了,要修改的是一个区间,但是仅仅是一个单点查询的功能
与一维的类似,二维树状数组自然是维护一个二维差分数组了(下面的 a 是原数组, d 是差分数组)
\(a_{n,m}=∑_{i=1}^n∑_{j=1}^md_{i,j}\)
但是区间修改操作呢?比如这里的区间增加操作。因为我们现在的二维树状数组维护的是原数组的二维差分数组,因此,我们类似于修改差分数组一样,单点修改二维树状数组
int lowbit(int x){
return x & -x;
}
void update(int x, int y, int k){
while (x <= n){
while (y <= m){
c[x][y] += k;
y += lowbit(y);
}
x += lowbit(x);
}
}
int getsum(int x, int y){
int sum = 0;
while (x > 0){
while (y > 0){
sum += c[x][y];
y -= lowbit(y);
}
x -= lowbit(x);
}
return sum;
}
void solve(){
// 将(x1, y1) ~ (x2, y2)区间加上或减去d
update(x1, y1, d);
update(x2 + 1, y1, -d);
update(x1, y2 + 1, -d);
update(x2 + 1, y2 + 1, d);
// (x, y)点的值
int sum = getsum(x, y);
}
5.3区间更新,区间查询
给定一个大小为 N×M 的零矩阵,直到输入文件结束,你需要进行若干个操作,操作有两类:
-
\(1 \quad a \quad b \quad c \quad d \quad x\),表示将左上角为 (a,b) ,右下角为 (c,d) 的子矩阵全部加上 x;
-
\(2 \quad a \quad b \quad c \quad d\),表示询问左上角为 (a,b) ,右下角为 (c,d) 为顶点的子矩阵的所有数字之和。
仍旧是利用差分,已知差分数组,求解前缀和sum
\(sum=\sum_{i=1}^n\sum_{j=1}^ma[i][j]=\sum_{i=1}^n\sum_{j=1}^m\sum_{k=1}^i\sum_{l=1}^jd[k][l]\)
对于单个d[i][j],有下图可知:

\(sum\)
\(=\sum_{i=1}^n\sum_{j=1}^m(n-i+1)(m-j+1)d[i][j]\)
\(=\sum_{i=1}^n\sum_{j=1}^m[nm-m(i-1)-n(j-1)+(i-1)(j-1)]d[i][j]\)
\(=nm\sum_{i=1}^n\sum_{j=1}^md[i][j] - m\sum_{i=1}^n\sum_{j=1}^m(i-1)d[i][j]-n\sum_{i=1}^n\sum_{j=1}^m(j-1)d[i][j]+\sum_{i=1}^n\sum_{j=1}^m(i-1)(j-1)d[i][j]\)
现在可以看到,我们实际上只需要维护\(d[i][j]\),\((i-1)d[i][j]\),\((j-1)d[i][j]\),\((i-1)(j-1)d[i][j]\) 就可以了
当我们要查询 (x,y) 位置的时候,只需要将对应系数带上就可以了
int lowbit(int x){
return x & -x;
}
void update(int x, int y, int k){
while (x <= n){
while (y <= m){
c1[i][j] += k;
c2[i][j] += (x - 1) * k;
c3[i][j] += (y - 1) * k;
c4[i][j] += (x - 1) * (y - 1) * k;
y += lowbit(y);
}
x += lowbit(x);
}
}
int getsum(int x, int y){
int sum = 0, i = x, j = y
while (i > 0){
while (j > 0){
sum += x * y * c1[i][j] + y * c2[i][j] + x * c3[i][j] + c4[i][j];
j -= lowbit(j);
}
i -= lowbit(i);
}
}
void solve(){
// 将(x1, y1) ~ (x2, y2)区间加上或减去d
update(x1, y1, d);
update(x2 + 1, y1, -d);
update(x1, y2 + 1, -d);
update(x2 + 1, y2 + 1, d);
// (x3, y3) ~ (x4, y4)区间的值
int sum = getsum(x4, y4) - getsum(x3 - 1, y4) - getsum(x4, y3 - 1) + getsum(x3 - 1, y3 - 1);
}

浙公网安备 33010602011771号