ACM_树状数组

2020.4.9 腾讯实习面试面到了这个题 太久没接触已经忘了 答得也不是很好

一、问题引入

维护数组看起来是十分简单的事情。
修改某点的值只要有下标直接就能改了,那对于求某个区间的和,怎么做呢?
我们可以使用一个前缀和的方法
对原数组 a [ 1 ] a [ 2 ] a [ 3 ] . . . a [ n ] a[1] a[2] a[3]... a[n] a[1]a[2]a[3]...a[n]
我们创建一个前缀和数组s
s [ 1 ] = a [ 1 ] , s [ 2 ] = a [ 1 ] + a [ 2 ] , s [ 3 ] = a [ 1 ] + a [ 2 ] + a [ 3 ] , . . . s [ n ] = s [ 1 ] + s [ 2 ] + . . . s [ n ] s[1]=a[1],s[2]=a[1]+a[2],s[3]=a[1]+a[2]+a[3], ... s[n]=s[1]+s[2]+...s[n] s[1]=a[1],s[2]=a[1]+a[2],s[3]=a[1]+a[2]+a[3],...s[n]=s[1]+s[2]+...s[n]
我们想要求区间l~r的和可以使用s[r]-s[l-1]来得到
假如我们想要单点修改呢?这样需要让该点之后的前缀和数组全部更新一遍!因此前缀和对于区间查询没有问题,对单点更新无法胜任。

二、树状树组来了

我们先来看看这究竟是个什么玩意。
这里写图片描述

A数组是原始n个数的数组,C数组就是是树状数组(“树状”数组,是指一个普通数组,按树状存储,而不是一种STL中的数据结构)。
这里写图片描述

C [ 1 ] = A [ 1 ] C [ 2 ] = C [ 1 ] + A [ 2 ] = A [ 1 ] + A [ 2 ] C [ 3 ] = A [ 3 ] C [ 4 ] = C [ 2 ] + C [ 3 ] + A [ 4 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] C [ 5 ] = A [ 5 ] C [ 6 ] = C [ 5 ] + A [ 6 ] = A [ 5 ] + A [ 6 ] C [ 7 ] = A [ 7 ] C [ 8 ] = C [ 4 ] + C [ 6 ] + C [ 7 ] + A [ 8 ] = A [ 1 ] + A [ 2 ] + A [ 3 ] + A [ 4 ] + A [ 5 ] + A [ 6 ] + A [ 7 ] + A [ 8 ] \begin{aligned} C[1] &= A[1] \\C[2] &= C[1] + A[2] = A[1] + A[2] \\C[3] &= A[3] \\C[4] &= C[2] + C[3] +A[4] = A[1] + A[2] + A[3] + A[4] \\C[5] &= A[5] \\C[6] &= C[5] + A[6] = A[5] + A[6] \\C[7] &= A[7] \\C[8] &= C[4] + C[6] + C[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8] \end{aligned} C[1]C[2]C[3]C[4]C[5]C[6]C[7]C[8]=A[1]=C[1]+A[2]=A[1]+A[2]=A[3]=C[2]+C[3]+A[4]=A[1]+A[2]+A[3]+A[4]=A[5]=C[5]+A[6]=A[5]+A[6]=A[7]=C[4]+C[6]+C[7]+A[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]

好像是有点规律,比如2的倍数后面跟着一大串的和。
这里写图片描述
那么这个k怎么求呢?我们要用到一个神奇的函数,请往下看

三、神奇的lowbit函数

l o w b i t ( x ) = x & ( − x ) = 2 k lowbit(x)=x\&(-x)=2^k lowbit(x)=x&(x)=2k,其中k为x在二进制下末尾0的个数

int lowbit(int x)
{
	return x&(-x);
}
//-t 代表t的负数 计算机中负数使用对应的正数的补码来表示
//例如 :
// t=6(0110) 此时 k=1
//-t=-6=(1001+1)=(1010)
// t&(-t)=(0010)=2=2^1

C [ i ] = A [ i − 2 k + 1 ] + A [ i − 2 k + 2 ] + . . . . . . A [ i ] ; C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; C[i]=A[i2k+1]+A[i2k+2]+......A[i];
C [ i ] = A [ i − l o w b i t ( i ) + 1 ] + A [ i − l o w b i t ( i ) + 2 ] + . . . . . . A [ i ] ; C[i]=A[i-lowbit(i)+1]+A[i-lowbit(i)+2]+......A[i]; C[i]=A[ilowbit(i)+1]+A[ilowbit(i)+2]+......A[i];
即把2^k都换成lowbit

四、树状数组的相关操作

getsum操作

我们想求 a [ 1 ] + a [ 2 ] + . . . + a [ x ] a[1]+a[2]+...+a[x] a[1]+a[2]+...+a[x]的和

int getsum(int x){
    int ans=0;
    for(int i = x; i ; i -= lowbit(i))//i要大于0
        ans+=C[i];
    return ans;
}

ok 下面利用C[i]数组,求A数组中前i项的和
举个例子 i=7;
sum[7]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7] ;
我们有
C[4]=A[1]+A[2]+A[3]+A[4]; C[6]=A[5]+A[6]; C[7]=A[7];
可以推出: sum[7]=C[4]+C[6]+C[7];
这里写图片描述

单点更新操作

当我们修改A数组中的某一个值时 应当如何更新C数组呢
回想一下区间查询的过程,再看一下上文中列出的图
当要动态改变一个数时,用刚刚的循环枚举出与它相关的位置,都增加(减少)即可。
牵一发而动全身!

void update(int k,int x){
    for(int i = k;i <= n;i += lowbit(i))
        C[i]+=x;
}

例如,当更新A[1]时,需要向上更新C[1], C[2], C[4], C[8]
写为二进制 C[(001)],C[(010)],C[(100)],C[(1000)]
1(001) C[1]+=A[1]
lowbit(1)=001 1+lowbit(1)=2(010) C[2]+=A[1]
lowbit(2)=010 2+lowbit(2)=4(100) C[4]+=A[1]
lowbit(4)=100 4+lowbit(4)=8(1000) C[8]+=A[1]

五、树状数组升级

区间修改

比如需要将x到y中每个数加上100,暴力O(N)。
怎么做呢?想想看
int i 循环x到y update(i,100) !!
那我要你树状数组何用???
但说明你已经开始掌握精髓了

引入一个概念 非常重要

差分

假设下标都从1开始。
设数组a[]={1,6,8,5,10},差分数组b[]={1,5,2,-3,5}
也就是说b[i]=a[i]-a[i-1]a[i]=b[1]+....+b[i](a相当于是b的前缀和)。
假如区间[2,4]都加上2的话
a数组变为a[]={1,8,10,7,10},b数组变为b[]={1,7,2,-3,3}
发现了没有,b数组只有b[2]和b[5]变了,因为区间[2,4]是同时加上2的,所以a数组该区间内相邻数的差值仍然不变。
所以对区间[x,y]进行修改,只用修改b[x]与b[y+1];
b[x]=b[x]+k;b[y+1]=b[y+1]-k;

六、例题

已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数数加上x
2.求出某一个数
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含2或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x 含义:输出第x个数的值
输出格式:
输出包含若干行整数,即为所有操作2的结果。
输入输出样例
输入样例1:
5 5
1 5 4 2 3
1 2 4 2
2 3
1 1 5 -1
1 3 5 7
2 4
输出样例1:
6
10

这道题属于单点查询+区间修改,我们可以对原数组的差分数组建立一个树状数组,使用getsum来做单点查询,使用差分数组的性质来做区间修改

#include <bits/stdc++.h>
using namespace std;
#define lowbit(i) i&(-i)
int a[1000000];
int tree[1000000];
int N,M;
int getsum(int x){
	int sum=0;
	for(int i=x;i;i-=lowbit(i)){
		sum+=tree[i];
	}
	return sum;
}
void insert(int x,int val){
	for(int i=x;i<=N;i+=lowbit(i)){
		tree[i]+=val;
	}
}
void update(int l,int r,int val){
	insert(l,val);//单点修改
	insert(r+1,-val);//单点修改
}
int main(){
	cin>>N>>M;
	for(int i=1;i<=N;i++){
		cin>>a[i];
		insert(i,a[i]-a[i-1]);//对差分数组构建树状数组
	} 
	for(int i=1;i<=M;i++){
		int a,b,c,d;
		cin>>a;
		if(a==1){
			cin>>b>>c>>d;
			update(b,c,d);//b到c全部加d
		}
		else{
			cin>>b;
			printf("%d\n",getsum(b));
		}
	}
	return 0;
}

需要区间求和呢?
我们需要额外一个数组来帮助我们~
例如,a是原数组,tree是我们的差分数组
我们有

a[1]=tree[1]
a[2]=tree[1]+tree[2]
a[3]=tree[1]+tree[2]+tree[3]
a[4]=tree[1]+tree[2]+tree[3]+tree[4]

类似的

 a[1]+a[2]+……+a[r-1]+a[r]
//用上方公式推导得出
=tree[1]+(tree[1]+tree[2])+……+(tree[1]+……+tree[r])
//根据加法交换律与结合律:
=(tree[1]*(r))+(tree[2]*(r-1))+……(tree[r]*1)
//那么:
=r*(tree[1]+tree[2]+……+tree[r])-(tree[1]*0+tree[2]*1+……+tree[r]*(r-1))

你看r*(一坨)可以用我们的升级版树状数组做,后面的那个规律也很明显
tree1[i]=tree[i]*(i-1)对吧!
好了
只要针对这个修改一下代码即可

题目描述
如题,已知一个数列,你需要进行下面两种操作:
1.将某区间每一个数加上x
2.求出某区间每一个数的和
输入输出格式
输入格式:
第一行包含两个整数N、M,分别表示该数列数字的个数和操作的总个数。
第二行包含N个用空格分隔的整数,其中第i个数字表示数列第i项的初始值。
接下来M行每行包含3或4个整数,表示一个操作,具体如下:
操作1: 格式:1 x y k 含义:将区间[x,y]内每个数加上k
操作2: 格式:2 x y 含义:输出区间[x,y]内每个数的和
输出格式:
输出包含若干行整数,即为所有操作2的结果。
输入输出样例
输入样例1:
5 5
1 5 4 2 3
2 2 4
1 2 3 2
2 3 4
1 1 5 1
2 1 4
输出样例1:
11
8
20

#include <bits/stdc++.h>
using namespace std;
#define ll long long
#define lowbit(i) i&(-i)
ll n,m;
ll tree[100010]={0};
ll tree1[100010]={0};
ll getsum(ll *array,ll x){
	ll sum=0;
	for(int i=x;i;i-=lowbit(i)){
		sum+=array[i];
	}
	return sum;
}
void insert(ll *array,ll index,ll val){
	for(ll i=index;i<=n;i+=lowbit(i)){
		array[i]+=val;
	}
}
int main(){
	ios::sync_with_stdio(false);
	cin>>n>>m;
	ll a,b=0;
	for(ll i=1;i<=n;i++){
		cin>>a;
		b=a-b;//差分思想
		insert(tree,i,b);
		insert(tree1,i,b*(i-1));
		b=a;
	}
	for(ll i=1;i<=m;i++){
		int t,x,y,z;
		cin>>t;
		if(t==1){
			cin>>x>>y>>z;
			insert(tree,x,z);
			insert(tree,y+1,-z);
			insert(tree1,x,z*(x-1));
			insert(tree1,y+1,-z*(y));
		}
		else{
			cin>>x>>y;
			cout<<getsum(tree,y)*y-(x-1)*getsum(tree,x-1)-(getsum(tree1,y)-getsum(tree1,x-1))<<endl;
		}
	}
	return 0;
}
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值