树状数组的基本操作

一、单点修改,区间查询

题目描述:

给出一个长度为n的序列,有m个操作,分别为询问[l,r]的区间和,和将x位置上的值增加C。

思路:

可以使用线性数组进行操作,对于每一次询问,修改的时间复杂度为O(1),询问的时间复杂度为O(n)。如果数量n较大,这种操作必定会超时,所以我们尝试用前缀和来维护这个数组。使用前缀和的方法,明显可以看出来,在每一次询问中,查询的时间复杂度为O(1),修改的时间复杂度为O(n)。所以这种方法也不太靠谱,那么在这种情况下我们就可以使用树状数组了。

提到树状数组我们就不得不提到三个函数:lowbit函数、修改函数和求区间和的函数,以下我们一个个介绍。

lowbit函数:

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

lowbit函数计算的是 一个数化为二进制从右往左看第一个为1位置代表的数,例如5的二级制101,那么lobit(5)=1。lowbit函数在树状数组里面用处很大。对于一个子节点a,a+lowbit(a)就是他的父节点,这样操作就可直接降低操作次数,从而达到降低时间复杂度的效果。

现在我们来理解一下这行代码:

我们知道,对于一个数的负数就等于对这个数取反+1

以二进制数11010为例:11010的补码为00101,加1后为00110,两者相与便是最低位的1

其实很好理解,补码和原码必然相反,所以原码有0的部位补码全是1,补码再+1之后由于进位那么最末尾的1和原码,最右边的1一定是同一个位置(当遇到第一个1的时候补码此位为0,由于前面会进一位,所以此位会变为1)。

所以我们只需要进行a&(-a)就可以取出最低位的1了
会了lowbit,我们就可以进行区间查询和单点更新了!!!
在这里插入图片描述

修改函数:

void add(int x,int c){//在下标为x的数组里加上c
        while(x<=n){ //n为序列的个数
        sum[x]+=c;
        x+=lowbit(x);
        }
}

修改函数很好理解,就是在每一个x位置及其父节点上加上需要加上的c就行了,主要是要理解为什么x会+=lowbit(x)。

查询函数:

int query(int x){
    int ans=0;
    while(x>0){
         ans+=sum[x];
         x-=lowbit(x);
    }
}

三个函数都理解了,我们来看个板子题。很简单,直接照猫画虎就行了。
https://loj.ac/problem/130​​​​

#include <iostream>
#include <cstring>
using namespace std;
const long long maxn = 1e6;
long long n, m, i, v, c[maxn + 10];
long long sum(long long x) {
    long long ans = 0;
    while (x > 0) {
        ans += c[x];
        x -= x & (-x);
    }
    return ans;
}
void update(long long x, long long v) {
    while (x <= n) {
        c[x] += v;
        x += x & (-x);
    }
}
int main() {
    cin >> n >> m;
    memset(c, 0, sizeof(c));
    for (i = 1; i <= n; i++) {
        cin >> v;
        update(i, v);
    }
    while (m--) {
        long long x, y, z;
        cin >> x >> y >> z;
        if (x == 1) {
            update(y, z);
        } else if (x == 2) {
            long long res = 0;
            res = sum(z) - sum(y - 1);
            cout << res << endl;
        }
    }
    return 0;
}

二、区间修改,单点查询

主要思想:差分。
设原数组为, 设原数组 a [ i ] a[i] a[i],差分数组为 d [ i ] d[i] d[i],则 d [ i ] = a [ i ] − a [ i − 1 ] ( i &gt; = 1 ) d[i]=a[i]-a[i-1] (i&gt;=1) d[i]=a[i]a[i1](i>=1),则 a [ i ] = ∑ j = 1 i d [ j ] a[i]=\sum_{j=1}^id[j] a[i]=j=1id[j],可以通过求的前缀和查询。
当给区间 [ l , r ] [l,r] [l,r]加上 x x x的时候, a [ l ] a[l] a[l]与前一个元素 a [ l − 1 ] a[l-1] a[l1]的差增加了 x x x a [ r + 1 ] a[r+1] a[r+1] a [ r ] a[r] a[r]的差减少了 x x x。根据数组 d [ i ] d[i] d[i]的定义,只需给 a [ l ] a[l] a[l]加上 x x x, 给 a [ r + 1 ] a[r+1] a[r+1]减去 x x x即可。

long long lowbit(ll x){
	return x&(-x);
}
void add(long long x,long long c){
		while(x<=n){
			sum[x]+=c;
			x+=lowbit(x);
		}
}
long long update_add(l,r,x){//修改的区间[l,r],修改的数值x
	add(l,x);
	add(r+1,-x);
}

传送门:https://loj.ac/problem/131

题目描述:
这是一道模板题。
给定数列 a [ 1 ] , a [ 2 ] , … , a [ n ] , a[1],a[2],\dots,a[n], a[1],a[2],,a[n],你需要一次进行 q q q个操作,操作有两类:
∙ \bullet 1 1 1 l l l r r r x x x : : : 对于所有 i i i ∈ \in [ l , r ] [l,r] [l,r],将 a [ i ] a[i] a[i]加上 x x x
∙ \bullet 2 2 2 i : i: i: 给定 i , 求 a [ i ] 的 值 。 i,求a[i]的值。 i,a[i]
保证 1 ≤ l ≤ r ≤ n , ∣ x ∣ ≤ 1 0 6 。 1\leq l\leq r\le n,|x|\leq 10^6。 1lrnx106
输出格式:
对于每个 2 2 2 i i i操作,输出一行,每行有一个整数,表示所求的结果。
样例
样例输入
3 2
1 2 3
1 1 3 0
2 2
样例输出
2
数据范围与提示:
对于所有数据, 1 ≤ n , q ≤ 1 0 6 , ∣ a [ i ] ∣ ≤ 1 0 6 , 1 ≤ l ≤ r ≤ n , ∣ x ∣ ≤ 1 0 6 。 1\leq n,q\leq 10^6,|a[i]|\leq 10^6,1\leq l\leq r\leq n,|x|\leq10^6。 1n,q106,a[i]106,1lrn,x106

#include <iostream>
#include <cstring>
using namespace std;
typedef long long ll;
const ll maxn = 1e6;
ll a[maxn + 5], n, m, sum[maxn + 5], i;
ll lowbit(ll x) { return x & (-x); }
void add(ll x, ll c) {
    while (x <= n) {
        sum[x] += c;
        x += lowbit(x);
    }
}
ll find(ll x) {
    ll ans = 0;
    while (x > 0) {
        ans += sum[x];
        x -= lowbit(x);
    }
    return ans;
}
int main() {
    memset(a, 0, sizeof(a));
    memset(sum, 0, sizeof(sum));
    scanf("%lld%lld", &n, &m);
    for (i = 1; i <= n; i++) {
        scanf("%lld", &a[i]);
        add(i, a[i] - a[i - 1]);
    }
    while (m--) {
        ll x;
        scanf("%lld", &x);
        if (x == 1) {
            ll l, r, y;
            scanf("%lld%lld%lld", &l, &r, &y);
            add(l, y);
            add(r + 1, -y);
        } else if (x == 2) {
            ll y;
            scanf("%lld", &y);
            printf("%lld\n", find(y));
        }
    }
}

三、区间修改,单点查询

主要思想:差分思想。
我们基于上一个问题的“差分”思路,考虑一下如何在构建的树状数组中求前缀和。由差分数组的定义我们可以得出 : a [ x ] = ∑ i = 1 x d [ i ] :a[x]=\sum^x_{i=1}d[i] a[x]=i=1xd[i],那么可以得出:
∑ i = 1 p a [ i ] = ∑ i = 1 p ∑ j = 1 i d [ j ] \sum^p_{i=1}a[i]=\sum^p_{i=1}\sum^i_{j=1}d[j] i=1pa[i]=i=1pj=1id[j]
上述式子还可以简化,我们可以看出 d [ 1 ] 用 了 p 次 , d [ 2 ] 用 了 p − 1 次 … … 依 次 类 推 d [ i ] 用 了 p − i + 1 次 , 所 以 讲 上 述 式 子 简 化 为 : d[1]用了p次,d[2]用了p-1次\dots\dots依次类推d[i]用了p-i+1次,所以讲上述式子简化为: d[1]p,d[2]p1d[i]pi+1
∑ i = 1 p ∑ j = 1 i d [ j ] = ∑ i = 1 p d [ i ] ∗ ( p − i + 1 ) = ( p + 1 ) ∗ ∑ i = 1 p d [ i ] − ∑ i = 1 p d [ i ] ∗ i \sum^p_{i=1}\sum^i_{j=1}d[j]=\sum^p_{i=1}d[i]*(p-i+1)=(p+1)*\sum^p_{i=1}d[i]-\sum^p_{i=1}d[i]*i i=1pj=1id[j]=i=1pd[i](pi+1)=(p+1)i=1pd[i]i=1pd[i]i
那么我们可以维护两个数组的前缀和:
s u m 1 [ i ] = d [ i ] sum1[i]=d[i] sum1[i]=d[i]
s u m 2 [ i ] = d [ i ] ∗ i sum2[i]=d[i]*i sum2[i]=d[i]i

long long lowbit(long long x){
 	return x&(-x);
} 
void add(long long x,long long y){
 	long long tx=x;
 	while(x<=n){
	    sum1[x]+=y;
	    sum2[x]+=tx*y;
  	    x+=lowbit(x);
        }
}
long long find(long long x){
	long long res=0;
 	long long tx=x;
 	while(x){
  	  res+=(tx+1)*sum1[x]-sum2[x];
  	  x-=lowbit(x);
 	}
 	return res;
}

搞懂了之后我们来看个模板题。
传送门:https://loj.ac/problem/132

题 目 描 述 : 题目描述:
这 是 一 道 模 板 题 。 这是一道模板题。
给 定 数 列 a [ 1 ] , a [ 2 ] , … , a [ n ] , 你 需 要 依 次 进 行 q 个 操 作 , 操 作 有 两 类 : 给定数列a[1], a[2], \dots, a[n],你需要依次进行q个操作,操作有两类: a[1],a[2],,a[n]q
∙ \bullet 1 1 1 l l l r r r x x x : 给 定 :给定 l l l r r r x x x 对 于 所 有 对于所有 i ∈ [ l , r ] i\in[l,r] i[l,r] 将 将 a [ i ] a[i] a[i] 加 上 加上 x ( 换 言 之 , 将 a [ l ] , a [ l + 1 ] , … , a [ r ] 分 别 加 上 x ) ; x(换言之,将 a[l], a[l+1], \dots, a[r]分别加上x); x(a[l],a[l+1],,a[r]x);
∙ \bullet l l l r r r : : : 给定 l l l r r r 求 ∑ i = l r a [ i ] 的 值 ( 换 言 之 , 求 a [ l ] + a [ l + 1 ] + ⋯ + a [ r ] 的 值 ) 。 求 \sum_{i=l}^ra[i]的值(换言之,求 a[l]+a[l+1]+\dots+a[r]的值)。 i=lra[i]a[l]+a[l+1]++a[r])
输 入 格 式 输入格式
第 一 行 包 含 2 个 正 整 数 n , q , 表 示 数 列 长 度 和 询 问 个 数 。 保 证 1 ≤ n , q ≤ 1 0 6 。 第一行包含2个正整数n,q,表示数列长度和询问个数。保证1\leq n,q\leq10^6。 2n,q,1n,q106
第 二 行 n 个 整 数 a [ 1 ] , a [ 2 ] , … , a [ n ] , 表 示 初 始 数 列 。 保 证 ∣ a [ i ] ∣ ≤ 1 0 6 。 第二行 n 个整数 a[1],a[2],\dots,a[n] ,表示初始数列。保证 |a[i]|\le 10^6。 na[1],a[2],,a[n]a[i]106
接 下 来 q 行 , 每 行 一 个 操 作 , 为 以 下 两 种 之 一 : 接下来 q 行,每行一个操作,为以下两种之一: q 1 1 1 l l l r r r x x x : 对 于 所 有 i ∈ [ l , r ] , 将 a [ i ] 加 上 x ; 2 l r : 输 出 ∑ i = l r a [ i ] 的 值 。 保 证 1 ≤ l ≤ r ≤ n , ∣ x ∣ ≤ 1 0 6 。 :对于所有 i\in[l,r] ,将 a[i] 加上 x ;2 l r:输出 \sum_{i=l}^ra[i] 的值。保证 1\le l\le r\le n, |x|\le 10^6 。 i[l,r]a[i]x2lri=lra[i]1lrn,x106
输 出 格 式 对 于 每 个 2 l r 操 作 , 输 出 一 行 , 每 行 有 一 个 整 数 , 表 示 所 求 的 结 果 。 输出格式对于每个 2 l r 操作,输出一行,每行有一个整数,表示所求的结果。 2lr
样 例 样 例 样例样例
输 入 输入
5 10
2 6 6 1 1
2 1 4
1 2 5 10
2 1 3
2 2 3
1 2 2 8
1 2 3 7
1 4 4 10
2 1 2
1 4 5 6
2 3 4
样 例 输 出 样例输出
15
34
32
33
50
数 据 范 围 与 提 示 对 于 所 有 数 据 , 1 ≤ n , q ≤ 1 0 6 , ∣ a [ i ] ∣ ≤ 1 0 6 , 1 ≤ l ≤ r ≤ n , ∣ x ∣ ≤ 1 0 6 。 数据范围与提示对于所有数据, 1\le n,q\le 10^6, |a[i]|\le 10^6 , 1\le l\le r\le n, |x|\le 10^6 。 1n,q106,a[i]106,1lrn,x106

#include<iostream>
#include<cstring>
using namespace std;
typedef long long ll;
ll n,m,i;
const ll maxn=1e6+5;
ll c1[maxn],c2[maxn];
ll a[maxn];
int lowbit(ll x){
    return x&(-x);
}
void add(ll p, ll x){
	ll y=p;
    for(;p<=n;p+=p&(-p))
        c1[p]+=x,c2[p] += x * y;
 
}
ll find(ll x){
    ll ans=0,y;
    y=x;
    for(;x;x-=x&(-x)){
        ans+=(y+1)*c1[x]-c2[x];
    }
    return ans;
} 
int main(){		
	scanf("%lld%lld",&n,&m);
 	memset(a,0,sizeof(a));
    	for(i=1;i<=n;i++)    scanf("%lld",&a[i]);
    	for(i=1;i<=n;i++){
        add(i,a[i]-a[i-1]);
    } 	
    	while(m--){
    	ll x;
    	scanf("%lld",&x);
    	if(x==1){
    		ll l,r,c;
    		scanf("%lld%lld%lld",&l,&r,&c);
    		add(l,c);
    		add(r+1,-c);
		}
    	else if(x==2){
    		ll l,r;
    		scanf("%lld%lld",&l,&r);
    		printf("%lld\n",find(r)-find(l-1));
		}
	}
    return 0; 
}

第一次写博客,如果哪里写错了,欢迎指出。

  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
树状数组(Fenwick Tree)是一种用于快速维护数组前缀和的数据结构。它可以在 $O(\log n)$ 的时间内完成单点修改和前缀查询操作,比线段树更加简洁高效。 下面是 Java 实现的树状数组详解: 首先,在 Java 中我们需要使用数组来表示树状数组,如下: ``` int[] tree; ``` 接着,我们需要实现两个基本操作:单点修改和前缀查询。 单点修改的实现如下: ``` void update(int index, int value) { while (index < tree.length) { tree[index] += value; index += index & -index; } } ``` 该函数的参数 `index` 表示要修改的位置,`value` 表示修改的值。在函数内部,我们使用了一个 `while` 循环不断向上更新树状数组中相应的节点,直到到达根节点为止。具体来说,我们首先将 `tree[index]` 加上 `value`,然后将 `index` 加上其最后一位为 1 的二进制数,这样就可以更新其父节点了。例如,当 `index` 为 6 时,其二进制表示为 110,最后一位为 2^1,加上后变为 111,即 7,这样就可以更新节点 7 了。 前缀查询的实现如下: ``` int query(int index) { int sum = 0; while (index > 0) { sum += tree[index]; index -= index & -index; } return sum; } ``` 该函数的参数 `index` 表示要查询的前缀的结束位置,即查询 $[1, index]$ 的和。在函数内部,我们同样使用了一个 `while` 循环不断向前查询树状数组中相应的节点,直到到达 0 为止。具体来说,我们首先将 `sum` 加上 `tree[index]`,然后将 `index` 减去其最后一位为 1 的二进制数,这样就可以查询其前一个节点了。例如,当 `index` 为 6 时,其二进制表示为 110,最后一位为 2^1,减去后变为 100,即 4,这样就可以查询节点 4 的值了。 最后,我们还需要初始化树状数组,将其全部置为 0。初始化的实现如下: ``` void init(int[] nums) { tree = new int[nums.length + 1]; for (int i = 1; i <= nums.length; i++) { update(i, nums[i - 1]); } } ``` 该函数的参数 `nums` 表示初始数组的值。在函数内部,我们首先创建一个长度为 `nums.length + 1` 的数组 `tree`,然后逐个将 `nums` 中的元素插入到树状数组中。具体来说,我们调用 `update(i, nums[i - 1])` 来将 `nums[i - 1]` 插入到树状数组的第 `i` 个位置。 到此为止,我们就完成了树状数组的实现。可以看到,树状数组的代码比线段树要简洁很多,而且效率也更高。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值