树状数组+差分数组(详细讲解+练习~)

本文介绍了树状数组(FenwickTree/BinaryIndexedTree)的数据结构,如何利用lowbit函数实现单点修改和区间查询的高效操作,以及与线段树的对比。通过示例和代码展示了如何运用树状数组解决相关问题,包括单点修改、区间查询和区间修改的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

一、引入

对于一个给定的序列,要想解决【单点修改,区间查询】问题:(区间查询即区间求和)

(1)对于普通数组而言,单点修改的时间复杂度是 O(1) ,但区间求和的时间复杂度是 O(n) 

(2)如果用前缀和的方法维护这个数组,区间求和的时间复杂度就降到了O(1),但是单点修改会影响后面所有的元素,时间复杂度是O(n)

(3)根据维基百科的定义:

A Fenwick tree or binary indexed tree is a data structure that can efficiently update elements and calculate prefix sums in a table of numbers.

        所谓树状数组,或称Binary Indexed Tree, Fenwick Tree,是一种用于高效处理对一个存储数字的列表进行更新及求前缀和的数据结构。树状数组可以比较轻松地处理【单点修改,区间查询】问题,时间复杂度都为o(logn)

        树状数组还与线段树有很大的联系,树状数组可以实现的功能线段树都可以实现,但有些功能只能由线段树来实现,但是相比较而言,线段树代码比较复杂,对于简单的问题用树状数组就好啦~

二、实现

        树状数组维护的数组下标必须从1开始!可以边看此文章边思考为什么哦~(原因在之后会讲到嘞!)

        虽说我们在讨论树状数组,它是一棵树,但是这棵树我们并不需要实实在在地建立起来,这只是我们脑中自己构想的数据结构罢了!

        树状数组的核心就是利用lowbit()函数来维护数组、建立树,以此使我们进行的相关操作的时间复杂度达到o(logn)

1. lowbit()函数

        该函数是用来求二进制数(从右往左数)第一个1以及后面的0的组成的值。

        例如,lowbit(10100)=100(都是二进制数)

        lowbit(x)= x &( -x ) ,x为二进制数。

(对于一个二进制数x,-x相当于x按位取反再加1,即求其补码。&就是将其补码和x按位运算。)

        那么为什么这样就可以求出来了呢?看下图~

        发现了什么没有?

        对于“(从右往左数)第一个1以及后面的0”x和(-x)是一样的,而前面的几位因为求反码取反了就是不一样的,因此求出来的就是“(从右往左数)第一个1以及后面的0”。

代码如下:

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

        对于更好地理解之后讲到的时间复杂度的计算,了解一下这个:

        对于二进制数x =0001,让它不断更新自己,使x= x + lowbit ( x ),直到x的值为2^{16},可以思考一下我们需要相加几次呢?

        答案是log2( 2^{16})=16次。那么如果我是让x即1,每次+1,则我得加多少次才能加完鸭是不。

2. 单点修改,区间查询

        接下来的问题就是我该怎么借助 lowbit() 来实现【单点修改,区间查询】等操作呢?

        首先看下图:(引用了其他博主的图哈~

https://blog.csdn.net/TheWayForDream/article/details/118436732

1)单点修改

        树状数组 tree [ i ] 维护的原数组a [ ] 区间为 ( i - lowbit ( i ) , i ] ,值为该区间之和。

        而且我们发现tree [ i + 1 ] = tree[ i + lowbit ( i ) ]。所以我们要进行单点修改下标为i的值,其他包含下标为i的值也是需要更新的,则只需一个循环来更新好t数组,主要见代码

void update(int i,int k){//对下标为i的值加上k 
	for(int pos=i;pos<=N;pos+=lowbit(pos)){
		tree[pos]+=k;
	}
}

时间复杂度为o(logn)。

2)区间查询

        对于区间查询,我们采用的是前缀和相减的方法,为什么要这样做呢?

        因为在树状数组里面求前缀和的时间复杂度为o(logn),挺快的呢。

        例如,从图中可以看出,a数组区间为[1,7]的和=tree[7]+tree[6]+tree[4],然而6=7-lowbit(7),4=6-lowbit(6),对于4-lowbit(4)<=1,所以就是t[7]、t[6]、t[4]三项之和。

代码如下:

int pre_sum(int a){
	int ans=0;
	for(int i=a;i>=1;i-=lowbit(i)){
		ans+=tree[i];
	}
	return ans;
}
int query(int a,int b){
	return pre_sum(b)-pre_sum(a-1);
}

        对于问题“树状数组维护的数组为啥下标必须从1开始”

        如果tree数组下标从0开始(for的条件变为x>=0),当x为1时x-=lowbit(x)即x=0这是正确的下一个循环x-=lowbit(x)即x还是=0。下下循环还是这样,因此sum会死循环加上tree[0]所以下标得从1开始嘞!!

学完这些之后就可以试一下这道题~

题目链接:https://www.luogu.com.cn/problem/P3374​​​​​​

代码:

#include<iostream>
#include<stdio.h>
#include<string.h>
#define lowbit(x) x&(-x)
#define MAXN 500010
typedef long long ll;
using namespace std;

ll a[MAXN],tree[MAXN];
int N,M; 

void update(int i,int k){//对下标为i的值加上k 
	for(int pos=i;pos<=N;pos+=lowbit(pos)){
		tree[pos]+=k;
	}
}
int pre_sum(int a){
	int ans=0;
	for(int i=a;i>=1;i-=lowbit(i)){
		ans+=tree[i];
	}
	return ans;
}
int query(int a,int b){
	return pre_sum(b)-pre_sum(a-1);
}
int main(){
	int op,x,y,k;
	cin>>N>>M;
	memset(tree,0,sizeof(tree));
	for(int i=1;i<=N;i++){
		cin>>a[i];
		update(i,a[i]);
	}
	for(int i=0;i<M;i++){
		cin>>op;
		if(op==1){//单点修改 
			cin>>x>>k;
			update(x,k);
		}else{//区间求和 
			cin>>x>>y;
			cout<<query(x,y)<<endl;
		}
	}
	return 0;
}
3. 区间修改,单点查询

        对于解决该问题,我们如果仅仅和【单点修改,区间查询】的方法一样,则时间复杂度将会变得很大。在这里,我们需要借助差分数组和树状数组用树状数组来维护差分数组实现快速解决问题。

1)差分数组

        对于区间修改(指的是在某一区间加上或减去同一个数),我们首先要学习差分数组

        例如,一个数组为:4,3,5,1,2。

        其差分数组i下标的值就是让原数组i下标的值减去i-1下标的值,即d [ i ] =a [ i ] - a [ i-1] 。当i=0时,d[i]=a[i],结果如下表所示:

下标

0

1

2

3

4

原数组a[]

4

3

5

1

2

差分数组d[]

4

-1

2

-4

1

前缀和

4

3

5

1

2

        对差分数组进行求前缀和,我们发现其值就是原数组的值!!!

        因此我们实现区间修改就很简单啦!

        例如,我们想让区间[1,3]的值加上3,只需要让d[1] + = 3, d [4] - = 3即可。为什么呢?思考一下~

下标

0

1

2

3

4

原数组a[]

4

3

5

1

2

差分数组d[]

4

-1 +3

2

-4 

1 -3

前缀和

4

3 +3

5 +3

1 +3

2 +3-3

        因为前缀和的影响,我们在差分数组中加上某一个数,则这个数会被前缀和传下去,要想根据我们的需求截断,就给d[3+1]减去该数,就抵消掉啦~

        因此,我们就可以使用差分数组快速实现区间修改!!!

        对于更好地理解差分数组,可以参考该文章练习相应题目。

        链接:https://blog.csdn.net/qq_44786250/article/details/100056975

2)单点查询

        对于【单点查询】查第 i 位置的值,我们只需要利用树状数组求差分数组的前 i 项前缀和即可。

        这种方法的时间复杂度会很低,都是o(logn)

        代码和【单点修改,区间查询】差不多,就把树状数组维护的对象变换一下。

       

学会了来练习一下吧!链接:https://www.luogu.com.cn/problem/P3368

代码:(自己写了之后再来看代码哦~)

#include<iostream>
#include<stdio.h>
#include<string.h>
#define lowbit(x) x&(-x)
#define MAXN 500010
typedef long long ll;
using namespace std;

ll a[MAXN],d[MAXN],tree[MAXN];
int N,M; 

void update(int i,int k){//对下标为i的值加上k 
	for(int pos=i;pos<=N;pos+=lowbit(pos)){
		tree[pos]+=k;
	}
}
int pre_sum(int a){
	int ans=0;
	for(int i=a;i>=1;i-=lowbit(i)){
		ans+=tree[i];
	}
	return ans;
}
int query(int a,int b){
	return pre_sum(b)-pre_sum(a-1);
}
int main(){
	int op,x,y,k;
	cin>>N>>M;
	memset(tree,0,sizeof(tree));
	for(int i=1;i<=N;i++){
		cin>>a[i];
		d[i]=a[i]-a[i-1];
		update(i,d[i]);
	}
	for(int i=0;i<M;i++){
		cin>>op;
		if(op==1){
			cin>>x>>y>>k;
			update(x,k);
			update(y+1,-k);
		}else{
			cin>>x;
			cout<<pre_sum(x)<<endl;
		}
	}
	return 0;
}

        再​​给到树状数组的一道应用题练习练习: https://www.luogu.com.cn/problem/P1908

三、结束

        对于解决【区间修改,区间查询】,树状数组可能不能实现,得依靠线段树。

        这篇文章就先讲到这啦,如有问题,请在评论区指出我会及时回复哒~拜拜~

<think>我们讨论树状数组(Fenwick Tree)结合差分数组(Difference Array)的应用。 首先,树状数组通常用于高效地计算前缀和以及支持单点更新。当我们使用差分数组技巧时,我们可以将区间更新(比如给区间[l, r]内的每个元素加上一个值x)转换为两个单点更新(在l位置加上x,在r+1位置减去x),然后通过求前缀和来得到原数组某个位置的值。 问题:树状数组使用差分数组的时候不用更新原数组吗? 回答:在树状数组结合差分技巧的应用中,我们实际上维护的是一个差分数组树状数组,而不是原始数组树状数组。因此,我们不需要直接更新原始数组,而是通过更新差分数组树状数组来实现区间更新和单点查询(或者区间查询,但需要额外的处理)。 具体来说: 1. 我们定义一个差分数组d,其中d[0]=a[0](假设原数组为a),d[i] = a[i] - a[i-1] (i>=1)。 2. 那么,原数组的前缀和可以表示为:a[i] = d[0] + d[1] + ... + d[i]。 3. 当我们给区间[l, r]加上x时,我们只需: d[l] += x d[r+1] -= x (如果r+1在数组范围内) 4. 然后,我们使用树状数组来维护这个差分数组d。这样,我们就可以快速计算d数组的前缀和,从而得到原数组a在任意位置i的值(即a[i]等于d[0]到d[i]的和)。 注意:树状数组在这里的作用是快速计算前缀和(即d[0..i]的和),以及支持对d数组的单点更新(即上面的两个操作)。 因此,我们并不需要显式地更新原数组a,因为我们可以通过树状数组查询得到任意位置的值(即前缀和)。但是,我们维护的树状数组实际上就是基于差分数组d的。 下面,我们给出一个使用树状数组维护差分数组的示例代码,实现区间更新和单点查询。 代码实现: 假设我们有一个长度为n的数组,初始全0。我们需要支持两种操作: 1. 给区间[l, r]的所有元素加上x。 2. 查询位置i的元素值。 步骤: - 初始化:树状数组的大小为n+1(索引从1到n),初始值为0。 - 对于区间更新[l, r, x]:在树状数组的l位置加上x,在r+1位置减去x(如果r+1<=n)。 - 对于单点查询i:求树状数组前缀和[1..i](即d[1]到d[i]的和,注意这里差分数组d[0]在树状数组中我们通常从1开始,所以d[0]我们不考虑,因为原数组初始为0,差分数组也是0,所以从1开始对应原数组的第一个元素)。 注意:原数组a[i]的值实际上就是差分数组d从1到i的前缀和(在树状数组中我们维护d数组,所以查询前缀和就是a[i]的值)。 代码示例(Python):</think>### 树状数组结合差分数组的原理 当树状数组(Fenwick Tree)与差分数组结合使用时,**不需要直接更新原数组**。差分数组本身已隐含了原数组的信息,树状数组的作用是高效维护差分数组的前缀和(即原数组的值)。以下是关键点: 1. **差分数组定义**: - 差分数组 `diff` 满足: `diff[1] = arr[1] - arr[0]` `diff[i] = arr[i] - arr[i-1]`(`i > 1`) - 原数组的值可通过前缀和还原: `arr[i] = diff[1] + diff[2] + ... + diff[i]` 2. **树状数组的作用**: - 树状数组维护的是**差分数组 `diff`**,而非原数组 `arr`。 - **区间更新**(`[l, r] + x`)转化为两个单点更新: - `diff[l] += x` → 树状数组 `update(l, x)` - `diff[r+1] -= x` → 树状数组 `update(r+1, -x)`(若 `r+1` 越界则省略) - **单点查询**(`arr[i]`)转化为求前缀和: `arr[i] = query(i)` 3. **原数组的状态**: - 原数组 `arr` 是**逻辑存在**的,其值由 `diff` 的前缀和决定。 - 无需额外维护 `arr`,因为任何位置的值都可通过 `query(i)` 实时计算。 --- ### 代码实现(Python) 以下代码实现支持区间更新和单点查询的树状数组(基于差分): ```python class FenwickTree: def __init__(self, n): self.n = n self.tree = [0] * (n + 1) # 树状数组(下标从1开始) def _lowbit(self, x): return x & -x # 获取最低位的1 def update(self, i, delta): # 单点更新:在位置i增加delta while i <= self.n: self.tree[i] += delta i += self._lowbit(i) def query(self, i): # 查询前缀和:diff[1]到diff[i]的和(即arr[i]) s = 0 while i > 0: s += self.tree[i] i -= self._lowbit(i) return s def range_update(self, l, r, delta): # 区间更新:[l, r] 增加delta self.update(l, delta) if r + 1 <= self.n: self.update(r + 1, -delta) # 差分的关键操作 # 示例使用 n = 5 # 数组长度 ft = FenwickTree(n) # 区间更新:[2,4] 每个元素加3 ft.range_update(2, 4, 3) # 单点查询:arr[3]的值 print(ft.query(3)) # 输出: 3 # 再次更新:[1,3] 每个元素加2 ft.range_update(1, 3, 2) # 查询arr[3](两次更新的叠加结果) print(ft.query(3)) # 输出: 3 + 2 = 5 ``` ### 关键解释 1. **`range_update` 方法**: - `ft.range_update(2, 4, 3)` → 等效于: - `diff[2] += 3`(树状数组 `update(2, 3)`) - `diff[5] -= 3`(树状数组 `update(5, -3)`,因 `r+1=5` 未越界) - 更新后 `arr[3] = diff[2] + diff[3] = 3 + 0 = 3` 2. **`query` 方法**: - `ft.query(3)` 计算 `diff[1] + diff[2] + diff[3]`,即 `arr[3]`。 3. **差分数组的隐含性**: - 代码中从未显式定义 `arr` 或 `diff` 数组,所有操作通过树状数组完成。 - 原数组的值通过 `query(i)` 动态计算得出。 --- ### 总结 - **无需更新原数组**:树状数组直接维护差分数组,原数组的值由前缀和实时推导。 - **高效性**:区间更新和单点查询的时间复杂度均为 **O(log n)**。 - **空间优化**:仅需 O(n) 空间存储树状数组
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值