树状数组(单点更新-区间查询,区间更新-单点查询,区间更新-区间查询,二维树状数组)

树状数组(单点更新-区间查询,区间更新-单点查询,区间更新-区间查询)

树状数组和线段树的比较:

1.都用来求区间问题,优化时间复杂度。它和线段树有着相似的功能,都能求,单点更新-区间查询,区间更新-单点查询,区间更新-区间查询 这些问题。

2.二者有着相似的时间复杂度。log()级的查询与修改。

3.树状数组的功能,用线段树完全能够实现,但是由线段树能实现的功能树状数组不一定能够实现。 线段树>树状数组。

4.既然功能不如线段树多,那为什么还要学树状数组呢?因为树状数组变成简单啊。代码少,写起来快啊~~。

树状数组的原理:

树状数组,顾名思义就是树状的数组。如图,就是一个变形的完全二叉树。
在这里插入图片描述
我们给每个块块都标上数值如下:
在这里插入图片描述
A[]是原数组,C[]就是我们的要维护的树状数组了。
由这个图我们可以看出
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]
这样我们更新A数组的时候 只需要更新与A相关的C数组就好,比如你要更新 A[1] 只需要更新与A[1] X相关的 C[1] C[2] C[4] C[8] 就好。
那么问题来了,我们这个C数组是怎么分的呢??转换给2进制就很明了了
C[1] = C[0001] = A[1];

    C[2] = C[0010] = A[1]+A[2];

    C[3] = C[0011] = A[3];

    C[4] = C[0100] = A[1]+A[2]+A[3]+A[4];

    C[5] = C[0101] = A[5];

    C[6] = C[0110] = A[5]+A[6];

    C[7] = C[0111] = A[7];

    C[8] = C[1000] = A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

C数组管辖的A的个数 = 2^k(个) k等于啥呢。 k就是这个二进制的从后往前连续0的个数,比如C[8] = C[1000] 后面连续三个0 所以k 就等于3 再比如C[6] =C[0110] 从后往前连续的0只有1个就断了 所以k=1.

那么 问题又来了,虽然我们明白了原理,知道了C 数组 怎么来的,我们怎么实现呢??
这里引入一个 lowbit(x) 函数,函数的功能就是取 x的最低位的1.代码很短。但是树状数组全靠它。

int lowbit(int x){
	return x&(-x);
}

取最后位的1 啥用呢?你想啊,我们每次更新的时候,要找相关的C 我们只要每次加一下lowbit不就好了嘛~
比如更新A[1] ,我们要更新的C分别得C[1] C[2] C[4] C[8]
1+lowbit(1) . 1的最低位的1还是1 所以1+lowbit(1) =2
2+lowbit(2) . 2=10 最低位的1 代表2 所以 2+lowbit(2) = 2+2 = 4;
4+lowbit(4) . 4=100 最低位的1 代表4 所以 4+lowbit(4)= 4+4= 8
同样,查询的时候我们只要往前查询就行了。每次减 lowbit()

单点更新,区间查询。

**更新操作:**正如我们前面所说的,更新的时候,只需要更新与它相关联的C数组就好,所以我们往上找,每次加lowbit()

void update(int x,int val)
{
	for(int i=x;i<=n;i+=lobwit(i)){
		c[i]+=val;
	}
}

查询操作:,查询应该查询往下找,每次减lowbit()
需要注意的是!! 我们 这样得到的结果是 A[1]+A[2]+…+A[N];

int query(int x){
	int res=1;
	for(int i=x;i>0;i-=lowbit(i)){
		res+=c[i];
	}
	return res;
}

我们要查询 L-R 这段区间的结果怎么查呢?
这个就很简单了啊,就是利用前缀和的思想嘛。

扫描二维码关注公众号,回复: 5999850 查看本文章
int query_range(int l,int r){
	return query(r)-query(l-1);
}

这样我们在主函数中,在输入数组A的时候 顺便更新着数组C 然后直接查询就好了。
例题:

HDU-1166 博客:https://blog.csdn.net/weixin_43179892/article/details/89493087
POJ-2352 博客:https://blog.csdn.net/weixin_43179892/article/details/89493357

区间更新,点查询

区间更新,点查询。
这时候我们维护的C数组,不再是和了 而是差。
给定一个数组A = 1 4 7 3 8
求相邻两个数组的差
B[1]=1-0=1;
B[2]=4-1=3;
B[3]=7-4=3
B[4]=3-7=-4
B[5]=8-3=5;
这是
A[2] = B[1]+B[2]
A[3] = B[1]+B[2] +B[3]

我们这时候对B,进行树状数组的操作。
查询操作:求第k个位置的数,只要查询 前k个位置的和就好了。
更新操作:如果要 L-R 的所有数都+ 2 我们只要更新 B[L]+=2 B[R+1]-=2就好了
举个例子,如果我们要 3-4的数每个都+2 我们让B[2]+=2=5 B[5]-=2;
此时你再求A[3]的时候 就是 b[1]+b[2]+b[3]=9 就是A[3]+=2 A[4]也类似
求A[5] = B[1]+B[2] +B[3] +B[4] +B[5] =8 还是原来的8 所以不会影响到后面的数。

本质上区间更新-点查询 和 点更新,区间查询是一样的,只是二者维护的东西不一样,一个是本身的值,而一个是差。 就连代码也基本相似,只是在主函数中,更新的时候稍有不同。
区间更新的代码。update 函数就跟点更新的一样。

void update_range(int l,int r,int val){
	update(l,val);
	update(r+1-val);
}

看一道例题基本上就能明白这个了。
例题:
HDU-1556 博客:https://blog.csdn.net/weixin_43179892/article/details/89493508
HDU-1754 博客:https://blog.csdn.net/weixin_43179892/article/details/89493821

区间更新,区间查询

区间更新,区间查询就是区间更新,点查询的延续
我们还是考虑差分的思想,我们设数组B还是存的两点之间的差。
单点查询的时候,我们只要一次query()就能该单点的值。
1-n的区间和怎么求呢?
A[1]=B[1];
A[2]=B[1]+B[2];
A[3]=B[1]+B[2]+B[3];
A[4]=B[1]+B[2]+B[3]+B[4];

A[n]=B[1]+B[2]+B[3]+B[4]+…B[n];

A[1]+A[2]+A[3]+…A[n] =
B[1]n +B[2](n-1)+B[3]*(n-1)+…B[n]*1=
(B[1]+B[2]+B[3]+…B[n])*n - (B[1]*0+B[2]*1+B[3]2+…B[n](n-1));
这就是简单的公式化简,应该不难看懂吧?
为什么这么化呢???我们来看这个式子

(B[1]+B[2]+B[3]+…B[n])*n - (B[1]*0+B[2]*1+B[3]2+…B[n](n-1));

前面这一部分不就是区间更新,单点查询中的那个单点查询嘛~
后面那一部分我们维护一个别的数组sum2 存的是 B[i]*(i-1),这样我们只要把两个数组查询结果一减就是我们要求的 1–n的和。 我们要查L-R的区间,还是用前缀和的思想。q®-q(l-1) (这里的q是查询函数)。

干看字可能看的很蒙,下面给出一个函数模板:

#include <bits/stdc++.h>
using namespace std;
const int maxn=1e5+10;
int sum1[maxn];//维护c1 + c2 + ....cn
int sum2[maxn];//维护 1*c1 + 2*c2 +...n*cn;
int a[maxn];
int lowbit(int x){
	return x&(-x);
}
void update(int x,int val){
	for(int i=x;i<=n;i+=lowbits(i)){
		sum1[i]+=val;
		sum2[i]+=(val)*(x-1);
	}
}
void update_range(int l,int r,int val){
	update(l,val);
	update(r+1,-val);
}
int query(int x){
	int res=0;
	for(int i=x;i>0;i-=lowbit(i)){
		res+= x*sum1[i]-sum2[i];
	}
	return res;
}
int query_range(int l,int r){
	return query(r)-query(l-1);
}

二维树状数组

二维树状数组解决什么问题呢?顾名思义就是二维的问题啦。
比如这么一个问题:
给你一个二维矩阵。给你两个操作:
A操作时 把x1 y1 的值加 上 val.
B操作时查询 x1 y1 x2 y2(分别表示矩形的左下角和 右上角) 这个矩形内的q权值和。

笨办法肯定是A操作的时候直接加val,B查询操作是就扫一遍需要查询的矩形,求一下和。但是这么傻的方法在 ACM比赛中 它百分百会超时的。

所以我们就引入了我们的二维树状数组。
二维树状数组其实和一维一模一样,就是把一维的变成二维的就好了。
*更新单点的时候,你不仅要更新x方向的,还要更新y方向的。那就是两次for循环的事。但是每一层的复杂度都是log级的,所以整个单点更新 是log()log() 级的

void updata(int x,int y,int num)
{
    int i,j;
    for(i=x; i<=n; i+=lowbit(i))
        for(j=y; j<=n; j+=lowbit(j))
            c[i][j]+=num;
}

同样,查询也是类似的,我们的query(x,y)函数求的是从(1,1)到(x,y)这个区间的和。这个也是x查 y查

int sum(int x,int y)//求(0,0)到 (x,y)之间的所有元素和
{
	int res=0;
	for(int i=x;i>0;i-=lowbit(i))
		for(int j=y;j>0;j-=lowbit(j))
			res+=c[i][j];
	return res;
}

但是我们要求的是 x1,y1,x2,y2,这个矩形内的和,这个地方就是二维前缀和的思想。
在这里插入图片描述
所以我们这么算就好了 (缝缝补补,凑出我们要的区间)
query(x2,y2)-query(x1-1,y2)-query(x2,y1-1)+query(x1-1,y1-1);

int query_range(int x1,int y1,int x2,int y2)
{
    return query(x2,y2)-query(x1-1,y2)-query(x2,y1-1)+query(x1-1,y1-1);
}

例题
POJ-1195 博客:https://blog.csdn.net/weixin_43179892/article/details/89494106

还会陆陆续续的补充一些。

猜你喜欢

转载自blog.csdn.net/weixin_43179892/article/details/89490577
今日推荐