【C++】树状数组

什么是树状数组?

顾名思义,就是用数组来模拟树形结构呗。那么衍生出一个问题,为什么不直接建树?答案是没必要,因为树状数组能处理的问题就没必要建树。和Trie树的构造方式有类似之处。

树状数组可以解决什么问题?

可以解决大部分基于区间上的更新以及求和问题。

树状数组和线段树的区别在哪里?

树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?树状数组的系数要少很多,就比如字符串模拟大数可以解决大数问题,也可以解决1+1的问题,但没人会在1+1的问题上用大数模拟。

树状数组的优点和缺点

  • 优点:修改和查询的复杂度都是O(logN),而且相比线段树系数要少很多,比传统数组要快,而且容易写。
  • 缺点:遇到复杂的区间问题还是不能解决,功能还是有限。

树状数组介绍

二叉树大家一定都知道,如下图:
在这里插入图片描述
如果每个父亲都存的是两个儿子的值,是不是就可以解决这类区间问题了呢。是的没错,但是这样的树形结构,叫做线段树。

那真的的树形结构是怎样的,和上图类似,但省去了一些节点,以达到用数组建树。

在这里插入图片描述
黑色数组代表原来的数组(下面用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;
}

树状数组的几种变式

1.单点修改、单点查询

传统数组可做

2.单点修改、区间查询

题目:https://loj.ac/problem/130
已讲解,详细看上面.

#include <bits/stdc++.h>

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;
}

3. 区间修改、单点查询

题目:https://loj.ac/problem/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 <bits/stdc++.h>

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;
}

4. 区间修改、区间查询

题目:https://loj.ac/problem/132
上面我们说的差值建树状数组,得到的是某个点的值,那如果我既要区间更新,又要区间查询怎么办。这里我们还是利用差分,由上面可知:
A[1]+A[2]+…+A[n]
= (D[1]) + (D[1]+D[2]) + … +(D[1]+D[2]+…+D[n])
= nD[1] + (n-1)D[2] +… +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 <bits/stdc++.h>

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)的区间的区间和。

1.单点修改、区间查询

题目:https://loj.ac/problem/133

#include <bits/stdc++.h>

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;
}

2.区间修改、单点查询

题目:https://loj.ac/problem/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 <bits/stdc++.h>

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;
}

3.区间修改 、区间查询

题目:https://loj.ac/problem/135
在这里插入图片描述
在这里插入图片描述

#include <bits/stdc++.h>

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;
}
发布了62 篇原创文章 · 获赞 75 · 访问量 2万+

猜你喜欢

转载自blog.csdn.net/Ljnoit/article/details/104476489