树状数组小结


前言


对于一个数组,我们常见的操作为修改和查询,那么对于一个长度为N的数组来说,我们查询和修改一个点的时间复杂度为O(1),而对于修改并查询一个区间的复杂度则为O(N),如果对于区间的操作远多于对单点的操作,那么我们就引入了差分数组与前缀和,将区间操作优化为了O(1),但单点的操作又变复杂了。
但我们能否在二者之间取一个折中,答案是可以的,我们的选择就是树状数组。


一、树状数组的作用

树状数组借助一种巧妙的划分,将修改与查询的时间复杂度维持在了O(log N),不论操作中修改与查询的次数,都成功避免了O(N)情况的产生,将整体的时间复杂度降低了一个数量级,随着数据量的增大,这个优势将体现的更加明显。


二、二进制拆分

在这里插入图片描述
一开始,我们可能不理解这个的划分方式,但借助下一张图,可能就明白了它的划分思路(感谢大佬的图)

当我们将每个下标转化为二进制表达,那么c[i]的中元素的个数为二进制第一个1的大小,而c[i]中一定是以a[i]作为结尾, 那么我们知道了是如何划分的,但这种划分方式又有什么优势呢?
我们以求A[1]到A[7]的和为例,如果是以前,我们需要每个数都访问一遍,再全部加上去,而现在,它的值为c[4]+c[6]+c[7],而7的二进制正是0111,所需要的元素个数与7的二进制有关,这就将操作的复杂度降为了O(log N)。


三、lowbit的使用

之前我们提到了c[i]的中元素的个数为二进制第一个1的大小,但是我们要如何求这个值呢,下面就介绍一种便于记忆和理解的方法。
首先我们要回顾一下,在计算机中,数据是以补码的形式来储存的,对于一个为负数的原码,如果我们将其转化为补码,有一个简单的方法:找到从右往左的第一个1,将其保留,再将1左侧的数字全部翻转。

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

通过一个简单的与运算,将除了那一个1以外其余的数都化为了0(x为一个正整数),这个返回的结果就是我们的目标。

可以拓展: x & & x = = l o w b i t ( x ) x\&\&x==lowbit(x) x&&x==lowbit(x)可以用来判断 x x x是否为 2 2 2的整数次幂。


四、基本使用

1.单点修改(前缀和)

在前缀和的学习中,如果我们需要修改单点的值,则需要修改所有包含该值的前缀和
而在树状数组中,可以以一种跳跃的方式进行访问,而不必像普通数组一样进行挨个的访问。

void add(int k,int num)
{
	while(k<=n)
	{
		c[k]+=num;
		k+=lowbit(k);
	}
}

2.区间查询(前缀和)

区间查询其实是求两个前缀和的差值。
我们以S[i]代表包含A[i]的前缀和,那么要求L到R的区间和,就是用S[R]-S[L-1]。

int SUM(int t){
	int sum=0;
	for(int i=t;i>=1;i-=lowbit(i))
		sum+=c[i];
	return sum;
}
int ask(int l,int r){
	return SUM(r)-SUM(l-1);
}

3.区间修改(差分数组)

以差分数组实现,区间修改只要对该区间的左端点进行操作后再对右端点进行相反操作

int change(int l.int r,int z){
	add(l,z);
	add(r+1,-z);
}

4.单点查询(差分数组)

在差分数组中,求单点的值其实就是求一个前缀和。
与上相同。

5.区间查询+区间修改

在差分数组中,可以简单的实现区间修改和单点查询,那么在求一个区间的和时,只要求出区间中每一个点的值将其合并就是区间的和。

我们原来的思路是反复进行单点查询,但经过公式的推导,得到了另一个公式,在这个公式里面出现了两个前缀和,分别为d[i]与d[i]*i的前缀和,那我们只要能求出这两个数组的前缀和就能够求出差分数组所表示的普通数组的前缀和,这就意味着我们可以求出区间和。
以下分别使用c1[i]跟c2[i]来分别表示着两个数组。

//区间修改
void add(int p,int x){//维护两个数组 
	for(int i=p;i<=n;i+=lowbit(i)){
		c1[i]+=x;
		c2[i]+=x*p;
	}
}
void range_add(int l,int r,int x){//区间修改 
	add(l,x);
	add(r+1,-x);
}
//区间查询
int ask(int p){
	int res=0;
	for(int i=p;i;i-=lowbit(i)){
		res+=(p+1)*c1[i]-c2[i];
	}
	return res;
} 
int range_ask(int l,int r){
	return ask(r)-ask(l-1);
}

五、进阶使用

1.求逆序对数

例题:求逆序对数

要解决这个问题,如果我们先不考虑数据的大小,那我们首先要理解什么是逆序对,一个逆序对中必定存在大小差异 ,像句废话
思路:
从左往右遍历这个序列,将每个数都作为逆序对中较小的那一方,再遍历这个数之前是否存在比它大的数,进行统计,最后将数据汇总就是序列中的总逆序对数。
具体实现:
在遍历序列时,对于一个数,先根据它的值将对应的数组元素(下标与数值相等)变为1,再看这个元素的后面(或前面,根据实际情况而定)是否有值为1的元素,这些元素个数之和就是以该数作为逆序对中较小值的逆序对的个数。
那么在这个过程中,我们需要频繁的进行区间访问和单点修改,非常适合用树状数组来实现。
注意:
在实现过程中,我们发现需要根据数值的大小来确定树状数组的大小,我们此时就需要进行离散化处理,在不改变原来顺序的前提下将原来的数值替换为更小的数值。

#include<bits/stdc++.h>
using namespace std;
int n;
struct node {
	int x;//编号 
	int y;//值 
};
int cmp(node u,node v){
	return u.y<v.y;
}
int CMP(node u,node v){
	return u.x<v.x;
}
node a[500005];
int b[500005];
int lowbit(int t){
	return t&(-t);
}
int search(int t){
	int sum=0;
	for(int i=t;i>=1;i-=lowbit(i))
		sum+=b[i];
	return sum;
}
int find(int l,int r){
	return search(r)-search(l-1);
}
void add(int t){
	if(find(t,t)==0){
		for(;t<=n;t+=lowbit(t)){
			b[t]+=1;
		}
	}
}
int main(){
	while(cin>>n&&n){
		memset(b,0,sizeof(b)); 
		for(int i=0;i<n;i++){
			a[i].x=i;
			cin>>a[i].y;
		}
		sort(a,a+n,cmp);
		for(int i=0;i<n;i++){
			a[i].y=i+1;
		}
		sort(a,a+n,CMP);
		long long sum=0;
		for(int i=0;i<n;i++){
			add(a[i].y);
			sum+=(find(a[i].y+1,n));
		}
		cout<<sum<<endl;
	}
}

2.区间求最值

例题:I Hate It
这道题原来是一道线段树的题目,但也可以用树状数组来求解。
那么我们现在就存在两个问题:如何求出一段区间的最值,如何进行更新。
我们的思路是在原来树状数组的基础上进行更新,原来的树状数组中存的是一段区间的和,而现在存的是区间 [i-lownit(i)+1,i]的最大值。
此时我们有两个数组,一个是原数组A[i],另一个是维护着最大值的树状数组H[i]。
更新操作:
每输入或修改一个A[i],就需要将所有包含A[i]的区段进行更新,通过lowbit进行跳跃式访问更新。

void update(int i, int val){
	while (i <= n){
		h[i] = max(h[i], val);
		i += lowbit(i);
	}
}

区间求最值:
在之前求区间和时,我们可以利用两个前缀和之差得到区间和,但在此时却无法用相同的方法。但回归本质,想要求一个区间的最值,那么肯定要沿一个方向对该区间内的每个元素进行比较,从而得到一个最值, 我们的基本思路也是这样的,只不过此时我们可以借助树状数组的便利性进行跳跃式访问,最终结束于左端点。

int query(int x, int y){
	int ans = 0;
	while (y >= x){
		ans = max(a[y], ans);
		y --;//这两句需要放在for语句的上面,防止for处理出结果后进行多余处理
		for (; y-lowbit(y) >= x; y -= lowbit(y))//判断是否过界,是否与左端点左侧的数据发生联系
			ans = max(h[y], ans);//若未过界,直接比较
		//在for中操作完后,若已经有结果将直接结束
	}
	return ans;
}

注意:
用scanf比cin更快一些,cin不适合接受大数据,cin会先将数据放入缓存区(不晓得什么意思 ),结果会超时。

#include <bits/stdc++.h>
using namespace std;
int a[200005], h[200005];
int n, m;
int lowbit(int x){
	return x & (-x);
}
void update(int i, int val){
	while (i <= n){
		h[i] = max(h[i], val);
		i += lowbit(i);
	}
}
int query(int x, int y){
	int ans = 0;
	while (y >= x){
		ans = max(a[y], ans);
		y --;
		for (; y-lowbit(y) >= x; y -= lowbit(y))
			ans = max(h[y], ans);
	}
	return ans;
}
int main(){
	int i, j, x, y, ans;
	char c;
	while (scanf("%d%d",&n,&m)!=EOF){
		for (i=1; i<=n; i++)
			h[i] = 0;
		for (i=1; i<=n; i++){
			scanf("%d",&a[i]);
			update(i,a[i]);
		}
		for (i=1; i<=m; i++){
			getchar();
			scanf("%c",&c);
			if (c == 'Q'){
				scanf("%d%d",&x,&y);
				ans = query(x, y);
				printf("%d\n",ans);
			}
			else if (c == 'U'){
				scanf("%d%d",&x,&y);
				a[x] = y;
				update(x,y);
			}
		}
	}
	return 0;
}

3.查询第K大/小的数

例题:The k-th Largest Group

4.二维树状数组

例题:Matrix
之前在讲前缀和的时候提过二维前缀和,此时的思路基本相同,只是把求前缀和的方式更新为了树状数组求前缀和。
(以上的所有基本操作在二维树状数组中都可以实现,包括区间修改和区间查询)
详情见于另一个大佬的博客树状数组 数据结构详解与模板
下面就写一下我自己关于二维树状数组的理解。
什么是二维树状数组:
其实我感觉有点像多重积分,先把列当成常数,再考虑行(先行后列亦可)
比如以b[i][j]为例,包括了原数组中[i-lowbit(i)+1,i]行的结果,而在每一行中又取得是[j-lowbit(j)+1,j]的结果。
以b[6][6]为例,等于a[5][5]+a[5][6]+a[6][5]+a[6][5].

如何进行单点更新:
根据上面的概念,此时仍然是访问所有包含目标的区段。

void update(int x, int y, int val)
{
	int i, j;
	for (i=x; i<=n; i+=lowbit(i))
		for (j=y; j<=n; j+=lowbit(j))
			h[i][j] += val;
}

区域更新:
这就跟之前二维前缀和一模一样了,分别对四个点进行处理。
注意四个点为:(x1, y1);(x1, y2+1);(x2+1, y1);(x2+1, y2+1);
求子矩阵和:

int Sum(int i, int j){
      int result = 0;
      for(int x = i; x > 0; x -= lowbit(x)) {
        for(int y = j; y > 0; y -= lowbit(y)) {
            result += b[x][y];
        }
      }
    return result;
   }

之后要求任意矩阵的和,操作跟上面差不多,几个矩形区域进行处理以下就好了。

题目代码:

#include <bits/stdc++.h>
using namespace std;
const int MAXN = 1010;
int b[MAXN][MAXN];
int n, m;
int lowbit(int x){
	return x & (-x);
}
void update(int x, int y, int val){
	int i, j;
	for (i=x; i<=n; i+=lowbit(i))
		for (j=y; j<=n; j+=lowbit(j))
			b[i][j]+= val;
}
int query(int x, int y){
	int i, j, ans = 0;
	for (i=x; i>0; i-=lowbit(i))
		for (j=y; j>0; j-=lowbit(j))
			ans += b[i][j];
	return ans;
}
int main(){
	int T, i, j, ans;
	int x, y, x1, y1, x2, y2;
	char c;
	cin>>T;
	while (T--){
		cin>>n>>m;
		memset(b,0,sizeof(b));
		for (i=1; i<=m; i++){
			getchar();
			cin>>c;
			if (c == 'C'){
				cin>>x1>>y1>>x2>>y2;
				update(x1, y1, 1);
				update(x1, y2+1, -1);
				update(x2+1, y1, -1);
				update(x2+1, y2+1, 1);
			}
			else if (c == 'Q'){
				cin>>x>>y;
				ans = query(x, y);
				ans %= 2;
				cout<<ans<<endl;
			}
		}
		if (T != 0)
			cout<<endl;
	}
	return 0;
}

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值