树状数组详解及其应用 区间更新+区间查询 逆序对

树状数组详解及其应用

前言:

之前以为树状数组搞懂了,结果一个树状数组题把我打回原形,原来我只是会背模板而已。。前来复习 学习一遍

为什么用树状数组

树状数组是一种存储数据的结构,通过压缩数据来达到高效的查询效率
在一些题目中需要多次查询区间和,同时更改点,如果我们暴力去更改和查询复杂度是O(n),如果用前缀和实现,虽然查询快但是更改是On,但是用树状数组维护,查询和更改都是O(logn),当然不只是单点修改+区间询问,在区间修改+区间询问,单点修改+区间最大值,逆序对查询都有很好的表现

怎么实现树状数组

树状数组怎么形成(有什么规律)

首先,树状数组和树形结构有什么关系呢

这是一颗树
这是
这是树状数组
在这里插入图片描述
可以发现树状数组其实就是树形结构整体向右拉,树状数组中每个点包含着一个或者多个值相加的结果
那么我们把树状数组和原数组的对应关系再写详细一些,看看其中的规律
C1 = A1
C2 = C1 + A2 = A1 + A2
C3 = A3
C4 = C2 + C3 + A4 = A1 + A2 + A3 + A4
C5 = A5
C6 = C5 + A6 = A5 + A6
C7 = A7
C8 = C4 + C6 + C7 + A8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8
可能现在看不出来规律,那我们把它写成二进制看看
在这里插入图片描述
观察树状数组中最后一位1的位置,可以发现,每个树状数组包含的原数组的个数等于树状数组二进制最后一位1和后面的数组成的数
比如6(110)最后一位1形成的数为2(10),所以C6 包含两项
因此总结出公式
C[i] = A[i - 2^k+1] + A[i - 2^k+2] + … + A[i];

lowbit函数(形成树状数组的关键)

刚刚所说的二进制最后一位1和后面的数组成的数,这么抽象,到底要怎么写呢
其实很简单,有两种写法

int lowbit1(int x) return x&(x^(x-1);
int lowbit2(int x) return x&-x;

这里两种写法都可以,首选第二种,因为更短,第二种巧妙利用了负数是补码的特性,
那么有了lowbi函数我们就可以轻松知道树状数组中下标为 i 包含原数组就是
[ i - lowbit(i), i ]

实际应用

单点修改 + 区间和询问
区间询问

这是树状数组中最为经典的用法
我们已经知道树状数组中下标为 i 包含原数组就是[ i - lowbit(i), i ],那么我们想要求出原数组[1,i]前缀和,那每次我们都计算[ i - lowbit(i), i ],然后让i减去它所包含的项目个数,也就是 i -= lowbit(i),直到出现i等于0,就可能把1到i的前缀和算出来

int getsum(int ix){      //求A[1 - ix]的和
    int res = 0;
    for(int i = ix;i>0;i-=lowbit(i)){
    	res += tree[i];
    }
    return res;
}
单点修改

由于在树状数组,每个值包含的不是原数组了,当修改单点时会影响后面多个树状数组值,当你修改了i的值时,后面包含该值的所有数都会变化
因为 A[i] 包含于 C[i + 2^k]、C[(i + 2^k) + 2^k],也就是你每次修改的下标都是i+lowbit(i)

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

所以单点修改 + 区间和询问的代码为

int A[100005],tree[400005]
int lowbit(int x) return x&-x;
void update(int ix,int x){  //在ix位置加上x值
	for(int i = ix;i<=n;i+=lowbit(i){
		tree[i] += x;
	}
}
int getsum(int ix){      //求A[1 - ix]的和
    int res = 0;
    for(int i = ix;i>0;i-=lowbit(i)){
    	res += tree[i];
    }
    return res;
}

区间修改 + 区间询问

可能你会问 区间修改 + 单点询问 呢?用两次区间询问相减就是单点询问了
但是如果我们只有一次单点询问,我们有更常用的方法那就是差分数组,有了这个启发,很容易去想用树状数组去实现差分数组
所以解决区间修改 + 区间询问就是差分树状数组

区间修改

对于普通的差分数组修改来说 [l,r]增加x 只需要 A[l] += x , A[r] += x,即可,那对于树状数组来说其实是一样的,树状数组就是数据结构,本质上还是个数组

void add(ll t[],ll ix,ll x){
    for(int i = ix;i<=n;i+=(i&-i)){
        t[i] += x;
    }
} 
cin>>c; 
add(tree, b + 1,-c);
add(tr,b+1,(b + 1)*-c);
区间询问

压力就给到了区间询问这边
在差分数组中我们查询单点时,需要计算前缀和,那区间询问我们就要计算多次前缀和吗? 显然不用,我们来看看规律
A1 = C1
A2 = C1 + C2
A3 = C1 + C2 + C3
A4 = C1 + C2 + C3 + C4

因此我们有 A n = ∑ i = 1 n C i A_n = \sum_{i=1}^n C_i An=i=1nCi

设Sn 是 区间和 ,那么
S1 = A1
S2 = A1 + A2
S3 = A1 + A2 + A3
S4 = A1 + A2 + A3 + A4
因此我们有 S n = ∑ i = 1 n A i S_n = \sum_{i=1}^n A_i Sn=i=1nAi
结合起来
S1 = C1
S2 = 2C1 + C2
S3 = 3C1 + 2C2 + C3
S4 = 4C1 + 3C2 + 2C3 + C4
因此我们有 S n = ∑ i = 1 n ∑ j = 1 i A j = ∑ i = 1 n ( n − i + 1 ) C i = ∑ i = 1 n ( n + 1 ) C i − ∑ i = 1 n i C i S_n = \sum_{i=1}^n \sum_{j=1}^i A_j =\sum_{i=1}^n(n-i+1)C_i=\sum_{i=1}^n(n+1)Ci-\sum_{i=1}^niC_i Sn=i=1nj=1iAj=i=1n(ni+1)Ci=i=1n(n+1)Cii=1niCi
所以我们计算区间和要用两个树状数组,一个记录 i*Ci的前缀和一个记录Ci的前缀和

ACwing243. 一个简单的整数问题2

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+7;
typedef long long ll;
ll tree[N],tr[N],n;
void add(ll t[],ll ix,ll x){
    for(int i = ix;i<=n;i+=(i&-i)){
        t[i] += x;
    }
} 
ll ask(ll t[],ll ix){
    ll res = 0;
    for(int i = ix;i>0;i-=(i&-i)){
        res += t[i];
    }
    return res;
}
ll A[N],m;
ll sum(ll ix){
    return (ix+1)*ask(tree,ix)-ask(tr,ix);
}
int main(){
    cin>>n>>m;
    int pre = 0;
    for(int i = 1;i<=n;i++){
        cin>>A[i];
        add(tree,i,A[i]-A[i-1]);
        add(tr,i,i*(A[i]-A[i-1]));
    }
    while(m--){
        char ch;
        int a,b,c;
        cin>>ch>>a>>b;
        if(ch=='Q'){
            cout<<sum(b)-sum(a-1)<<endl;
        }
        else {
            cin>>c;
            add(tree,a,c), add(tree, b + 1,-c);
            add(tr,a,a*c), add(tr,b+1,(b + 1)*-c);
        }
    }
    return 0;
}

区间最大值 /最小值

如果你已经了解树状数组的形成,那么对于区间最大值和区间和来说是差不多的,只不过一个取最大值,一个是累加

区间修改

对于区间修改,考虑修改一个点对于后面的最大值影响,这和区间和的想法基本一致

void update(int ix ,ll x){
	for(int i = ix;i<=n;i+=(i&-i))
	tree[i] = max(x,tree[i]);
}
区间查询最大值

对于区间查询我们不能像查询区间和一样用两次前缀和相减解决,因为区间最大值只跟区间内的元素有关
所以我们要回到树状数组定义中,直接在区间内找最大值
那如何查询
在这里插入图片描述
由于C【i】 是包含 [i-lowbit(i),i ]的最大值的,如果l 大于 i - lowbit 时意味着查询区间时不合法的,所以我们不能直接拿C【i】取最大值,我们只能 取原数组A【i】的最大值,然后 查询i-1的位置,直到到达l。
例如此时我们查询(3,6)的最大值,对于C【6】包含[5,6 ]的最大值,是大于3的,是合法的,C【6】可能最大值,然后 6 - lowbit(6) ,也就是查询 [3,4] , 此时C【4】包含[1,4] 显然是不合法的,只能取原数组A[4]的最大值,然后查询[3,3] ,然后包含[3,3]合法,就取C【3】的最大值
所以查询(3,6)的最大值 = max{C[6],A[4],C[3]}

ll ask(int l,int r){
	ll ans=A[r];
	while(l<=r){	
		for(;r-(r&-r)+1>=l&&r>=l;r-=(r&-r)){
			ans = max(tree[r],ans);
		}
		if(l<=r)ans = max(A[r],ans);
		r--;
	}
	return ans;
}

总代码

#include<bits/stdc++.h>
using namespace std;
const int N = 1e6+7;
typedef long long ll;
ll tree[N],A[N];
ll n;
void update(int ix ,ll x){
	for(ll i = ix;i<=n;i+=(i&-i))
	tree[i] = max(x,tree[i]);
}
ll ask(int l,int r){
	ll ans=A[r];
	while(l<=r){	
		for(;r-(r&-r)+1>=l&&r>=l;r-=(r&-r)){
			ans = max(tree[r],ans);
		}
		if(l<=r)ans = max(A[r],ans);
		r--;
	}
	return ans;
}
int main(){
	int T;
	cin>>n;
	memset(tree,-0x3f,sizeof(tree));
	for(int i = 1;i<=n;i++){
		cin>>A[i];
		update(i,A[i]);
	}
	cin>>T;
	while(T--){
		int b,c;
		cin>>b>>c;
		cout<<ask(b,c)<<endl;
	}
	return 0;
}

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值