顾名思义,就是用数组来模拟树形结构呗。那么衍生出一个问题,为什么不直接建树?答案是没必要,因为树状数组能处理的问题就没必要建树。和Trie树的构造方式有类似之处。
可以解决大部分基于区间上的更新以及求和问题。
树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?树状数组的系数要少很多,就比如字符串模拟大数可以解决大数问题,也可以解决1+1的问题,但没人会在1+1的问题上用大数模拟。
二叉树大家一定都知道,如下图:
如果每个父亲都存的是两个儿子的值,是不是就可以解决这类区间问题了呢。是的没错,但是这样的树形结构,叫做线段树。
那真的的树形结构是怎样的,和上图类似,但省去了一些节点,以达到用数组建树。
黑色数组代表原来的数组(下面用A[i]代替),红色结构代表我们的树状数组(下面用C[i]代替),发现没有,每个位置只有一个方框,令每个位置存的就是子节点的值的和,则有:
C[1] = A[1];
C[2] = A[1] + A[2];
C[3] = A[3];
C[4] = A[1] + A[2] + A[3] + A[4];
C[5] = A[5];
C[6] = A[5] + A[6];
C[7] = A[7];
C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];
可以发现,这颗树是有规律的
C[i] = A[i - 2^k+1] + A[i - 2^k+2] + … + A[i];
//k为i的二进制中从最低位到高位连续零的长度
例如i = 8(1000)时候,k = 3,可自行验证。
这个怎么实现求和呢,比如我们要找前7项和,
那么应该是SUM = C[7] + C[6] + C[4];
而根据上面的式子,容易的出SUMi = C[i] +C[i-2^k1] + C[(i - 2^k1) - 2^k2] + …;
其实树状数组就是一个二进制上面的应用。
现在新的问题来了2^k 该怎么求呢,不难得出 2^k = i&(i^(i-1)); 但这个还是不好求出呀,前辈的智慧就出来了,2^k = i&(-i);
上面已经解释了如何用树状数组求区间和,那么如果我们要更新某一个点的值呢,还是一样的,上面说了C[i] = A[i - 2^k+1] + A[i - 2^k+2]+ … + A[i],那么如果我们更新某个A[i]的值,则会影响到所有包含有A[i]位置。如果求A[i]包含哪些位置里呢,同理有:A[i] 包含于 C[i + 2^k1]、C[(i + 2^k1) +2^k2]…;
模板:
int n;
int a[N],C[N]; //对应原数组和树状数组
int lowbit(int x) {
return x&(-x);
}
void updata(int i,int k) { //在i的位置上加k
while(i<=n) {
c[i]+=k;
i+=lowbit(i);
}
}
int getsum(int i) { //求a[1]~a[i]的和
int res=0;
while(i>0) {
res += c[i];
i-=lowbit(i);
}
return res;
}
传统数组可做
题目:https://loj.ac/problem/130
https://vjudge.net/problem/LibreOJ-130
已讲解,详细看上面.
#include
using namespace std;
#define ll long long
#define N 1000000 + 10
int n, q;
ll a[N];
inline void add(int x, ll v) {
while (x <= n) a[x] += v, x += x & -x;
}
ll getsum(int x) {
ll ret = 0;
while (x) ret += a[x], x -= x & -x;
return ret;
}
int main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i++) {
ll x;
scanf("%lld", &x);
add(i, x);
}
while (q--) {
int k, l, r;
scanf("%d%d%d", &k, &l, &r);
if (k == 2) printf("%lld\n", getsum(r) - getsum(l - 1));
else add(l, r);
}
return 0;
}
题目:https://loj.ac/problem/131
https://vjudge.net/problem/LibreOJ-131
这就是第一个问题,如果题目是让你把x-y区间内的所有值全部加上k或者减去k,然后查询操作是问某个点的值,这种时候该怎么做呢。如果是像上面的树状数组来说,就必须把x-y区间内每个值都更新,这样的复杂度肯定是不行的,这个时候,就不能再用数据的值建树了,这里我们引入差分,利用差分建树。
A[] = 1 2 3 5 6 9
D[] = 1 1 1 2 1 3
如果我们把[2,5]区间内值加上2,则变成了
A[] = 1 4 5 7 8 9
D[] = 1 3 1 2 1 1
发现了没有,当某个区间[x,y]值改变了,区间内的差值是不变的,只有D[x]和D[y+1]的值发生改变,至于为什么我想我就不用解释了吧。所以我们就可以利用这个性质对D[]数组建立树状数组,代码为:
#include
using namespace std;
#define ll long long
#define N 1000000 + 10
int n, q;
ll a[N], c[N];
inline void add(int x, ll v) {
while (x <= n) c[x] += v, x += x & -x;
}
ll getsum(int x) {
ll ret = 0;
while (x) ret += c[x], x -= x & -x;
return ret;
}
int main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
add(i, a[i] - a[i - 1]);
}
while (q--) {
int k, l, r, x;
scanf("%d", &k);
if (k == 1) {
scanf("%d%d%d", &l, &r, &x);
add(l, x);
add(r + 1, -x);
} else {
scanf("%d", &x);
printf("%lld\n", getsum(x));
}
}
return 0;
}
题目:https://loj.ac/problem/132
https://vjudge.net/problem/LibreOJ-132
上面我们说的差值建树状数组,得到的是某个点的值,那如果我既要区间更新,又要区间查询怎么办。这里我们还是利用差分,由上面可知:
A [ 1 ] + A [ 2 ] + . . . + A [ n ] A[1]+A[2]+...+A[n] A[1]+A[2]+...+A[n]
= ( D [ 1 ] ) + ( D [ 1 ] + D [ 2 ] ) + . . . + ( D [ 1 ] + D [ 2 ] + . . . + D [ n ] ) = (D[1]) + (D[1]+D[2]) + ... +(D[1]+D[2]+...+D[n]) =(D[1])+(D[1]+D[2])+...+(D[1]+D[2]+...+D[n])
= n ∗ D [ 1 ] + ( n − 1 ) ∗ D [ 2 ] + . . . + D [ n ] = n*D[1] + (n-1)*D[2] +... +D[n] =n∗D[1]+(n−1)∗D[2]+...+D[n]
= n ∗ ( D [ 1 ] + D [ 2 ] + . . . + D [ n ] ) − ( 0 ∗ D [ 1 ] + 1 ∗ D [ 2 ] + . . . + ( n − 1 ) ∗ D [ n ] ) = n*(D[1]+D[2]+...+D[n]) -(0*D[1]+1*D[2]+...+(n-1)*D[n]) =n∗(D[1]+D[2]+...+D[n])−(0∗D[1]+1∗D[2]+...+(n−1)∗D[n])
所以我们只要维护一个d[1],d[2]…d[n]的树状数组,和一个(i-1)*d[i]的树状数组即可。
#include
using namespace std;
#define N 1000000 + 10
#define ll long long
int n, q;
ll a[N], b[N], c[N];
inline void add1(int x, ll v) {
while (x <= n) b[x] += v, x += x & -x;
}
inline void add2(int x, ll v) {
while (x <= n) c[x] += v, x += x & -x;
}
ll getsum1(int x) {
ll ret = 0;
while (x) ret += b[x], x -= x & -x;
return ret;
}
ll getsum2(int x) {
ll ret = 0;
while (x) ret += c[x], x -= x & -x;
return ret;
}
int main() {
scanf("%d%d", &n, &q);
for (int i = 1; i <= n; i++) {
scanf("%lld", &a[i]);
add1(i, a[i] - a[i - 1]);
add2(i, (i - 1) * (a[i] - a[i - 1]));
}
while (q--) {
int k, l, r;
ll x;
scanf("%d", &k);
if (k == 1) {
scanf("%d%d%lld", &l, &r, &x);
add1(l, x);
add1(r + 1, -x);
add2(l, (l - 1) * x);
add2(r + 1, -r * x);
} else {
scanf("%d%d", &l, &r);
printf("%lld\n", r * getsum1(r) - (l - 1) * getsum1(l - 1) - (getsum2(r) - getsum2(l - 1)));
}
}
return 0;
}
我们已经学会了对于序列的常用操作,那么我们不由得想到(谁会想到啊)……能不能把类似的操作应用到矩阵上呢?这时候我们就要写二维树状数组了!
在一维树状数组中,tree[x](树状数组中的那个“数组”)记录的是右端点为x、长度为lowbit(x)的区间的区间和。
那么在二维树状数组中,可以类似地定义tree[x][y]记录的是右下角为(x, y),高为lowbit(x), 宽为 lowbit(y)的区间的区间和。
题目:https://loj.ac/problem/133
https://vjudge.net/problem/LibreOJ-133
#include
using namespace std;
#define ll long long
#define N 4500
ll t[N][N];
int n, m;
void add(int x, int y, int k) {
for (int i = x; i <= n; i += i & -i)
for (int j = y; j <= m; j += j & -j) t[i][j] += k;
}
ll calc(int x, int y) {
ll ret = 0;
for (int i = x; i; i -= i & -i)
for (int j = y; j; j -= j & -j) ret += t[i][j];
return ret;
}
ll solve(int a, int b, int c, int d) {
return calc(c, d) - calc(c, b - 1) - calc(a - 1, d) + calc(a - 1, b - 1);
}
int main() {
scanf("%d%d", &n, &m);
int k, x, y, a, b, c, d;
while (scanf("%d", &k) != EOF) {
if (k == 1) {
scanf("%d%d%d", &x, &y, &k);
add(x, y, k);
} else {
scanf("%d%d%d%d", &a, &b, &c, &d);
printf("%lld\n", solve(a, b, c, d));
}
}
return 0;
}
题目:https://loj.ac/problem/134
https://vjudge.net/problem/LibreOJ-134
我们对于一维数组进行差分,是为了使差分数组前缀和等于原数组对应位置的元素。
那么如何对维数组进行差分呢? 可以针对二维前缀和的求法来设计方案。
维前缀和:
sum[i][j] = sum[i- 1][j] + sum[i][j- 1]- sum[i- 1][j- 1] + a[i][j]
那么我们可以令差分数组d[i][j]表示a[i][j]与a[i- 1][j] +a[i][j- 1]-a[i- 1][j- 1]的差。.
例如下面这个矩阵:
1 4 8
6 7 2
3 9 5
对应的差分数组就是:
1 3 4
5 -2 -9
-3 5 1
当我们想要将一个矩阵加上x时,怎么做呢?
下面是给最中间的3*3矩阵加上x时,差分数组的变化:
0 0 0 0 0
0 +x 0 0 -x
0 0 0 0 0
0 0 0 0 0
0 -x 0 0 +x
这样给修改差分,造成的效果就是:
0 0 0 0 0
0 x x x 0
0 x x x 0
0 x x x 0
0 0 0 0 0
#include
using namespace std;
#define ll long long
#define N (1 << 12) + 1
ll tree[N][N];
int n, m;
void add(int a, int b, int k) {
for (int i = a; i <= n; i += i & -i)
for (int j = b; j <= m; j += j & -j) tree[i][j] += k;
}
ll query(int a, int b) {
ll sum = 0;
for (int i = a; i > 0; i -= i & -i)
for (int j = b; j > 0; j -= j & -j) sum += tree[i][j];
return sum;
}
void insert(int a, int b, int c, int d, int k) {
add(a, b, k);
add(c + 1, d + 1, k);
add(a, d + 1, -k);
add(c + 1, b, -k);
}
int main() {
scanf("%d%d", &n, &m);
int flag;
while (~scanf("%d", &flag)) {
if (flag == 1) {
int a, b, c, d, k;
scanf("%d%d%d%d%d", &a, &b, &c, &d, &k);
insert(a, b, c, d, k);
}
if (flag == 2) {
int x, y;
scanf("%d%d", &x, &y);
cout << query(x, y) << endl;
}
}
return 0;
}
题目:https://loj.ac/problem/135
https://vjudge.net/problem/LibreOJ-135
#include
using namespace std;
#define lowbit(x) ((x) & (-x))
#define ll long long
int const N = 2048 + 10;
int n, m;
ll s1[N][N], s2[N][N], s3[N][N], s4[N][N];
void add(int x, int y, int v) {
for (int i = x; i <= n; i += lowbit(i))
for (int j = y; j <= m; j += lowbit(j)) {
s1[i][j] += v;
s2[i][j] += (ll)x * v;
s3[i][j] += (ll)y * v;
s4[i][j] += (ll)x * y * v;
}
}
ll getsum(int x, int y) {
ll ret = 0;
for (int i = x; i; i -= lowbit(i))
for (int j = y; j; j -= lowbit(j)) {
ret += (ll)(x + 1) * (y + 1) * s1[i][j];
ret -= (ll)(y + 1) * s2[i][j];
ret -= (ll)(x + 1) * s3[i][j];
ret += s4[i][j];
}
return ret;
}
int main() {
scanf("%d%d", &n, &m);
int x, a, b, c, d, k;
while (scanf("%d", &k) != EOF) {
if (k == 1) {
scanf("%d%d%d%d%d", &a, &b, &c, &d, &x);
add(a, b, x);
add(a, d + 1, -x);
add(c + 1, b, -x);
add(c + 1, d + 1, x);
} else {
scanf("%d%d%d%d", &a, &b, &c, &d);
printf("%lld\n", getsum(c, d) - getsum(a - 1, d) - getsum(c, b - 1) + getsum(a - 1, b - 1));
}
}
return 0;
}