高级数据结构:树状数组详解(c++实现 求解动态数组区间和问题)_c++高级数据结构-程序员宅基地

技术标签: 算法  c++  高级数据结构  数据结构  

引入

什么是树状数组???

解决数据压缩里的累积频率(Cumulative Frequency)的计算问题,现多用于高效计算数列的前缀和, 区间和,这样的问题可以使用树状数组算法来解决。

  • 前面我们讲过求静态数组的子区间和,我们可以利用前缀和来求得
  • 而现在我们引入动态数组的子区间和这一问题,我们可以使用树状数组。

我们现在通过修改数组,往数组中某一个位置增加或减小一个值,从而导致前缀和数组会发生变化,因此我们不可以使用普通前缀和这一思路来做。
考虑一下:我们求静态数组的前缀和时,如果我们使用暴力法,使用两个for循环来枚举子区间之和,从而得到静态数组的区间和,但是这一复杂度会达到O(N^2),因此使用普通的前缀和可以将复杂度达到O(N)。但是当我们在动态数组中修改元素时,每次修改元素会导致原来的前缀和发生变化,因此我们还需要维护前缀和,这样的时间复杂度也会达到O( N^2)。

但是如果我们使用树状数组这一算法,时间复杂度将会降为O(nlog2n),接下来我将详细解释一下树状数组


树状数组介绍

树状数组包含三个基本的功能:

  • 维护前缀和数组:在某个位置增加减少值时,需要调用此函数,来动态维护前缀和
  • 查询前缀和:求某个区间 [l,r]的区间和,即 sum[r]-sum[l-1]
  • lowbit运算:这个概念非常重要,用于得到某个数的二进制的最低位的一,有什么用呢,我们待会再说。

首先定义tree数组表示一个树状数组tree[i]表示第i个位置的区间的和,区间[l,r]的和即为tree[r]-tree[l-1]

  • 首先来看维护前缀和数组:update的函数意思是在数组的 第i个元素加上元素x,x可以是正负,分别代表tree[i]加或者减一个值
//维护
void update(int i,int x)
{
    
	/*
	i:下标
	x:增量
	维护:在末尾的1上加1
	*/
	while (i <= n)
	{
    
		tree[i] += x;
		i += lowbit(i);
	}
}
  • 查询区间的和: 求[l,r]的区间和即为 sum[r]-sum[l-1]
//求和
int sum(int i)
{
    
	/*
	求和(查询):去除末尾的1
	*/
	int num = 0;
	while (i)
	{
    
		num += tree[i];
		i -= lowbit(i);
	}
	return num;
}
  • lowbit操作:当前一个数字的补码与原数相与得到的便是当前数字二进制的最后一个1。
inline int lowbit(int x)
{
    
	return ((x) & (-x));
}


接下来详细解释一个这三个函数基本功能:

引用《百度百科》的一张图来解释
在这里插入图片描述

在这里插入图片描述

对于查询的过程:每次去掉二进制的最后的1,例如我们查询sum(7):

  • 7的二进制是 111 ,去掉最后的1,得到:110,即十进制为6,则sum(6)
  • 6的二进制是110,去掉最后的1,得到:100,即十进制为4,则sum(4)
  • 4的二进制是100,去掉最后的1,得到:0,为零则结束

所以说查询sum(7)就等于:sum(7) = sum(6)+sum(4),如上图的蓝色表示。


对于维护的过程:每次在二进制数最后的1上加1,例如我们更新update(1,2):

  • 1的二进制是1,在最后一个1上加1得10,十进制为2,所以说tree[2]也会跟着修改
  • 2得二进制是10,在最后一个1上加1得100,十进制为4,所以说tree[4]也会跟着修改
  • 4得二进制是100,在最后一个1上加1得1000,十进制为8,所以说tree[8也会跟着修改

所以说当维护(修改一个位置的元素值)时,我们会自动动态的维护整个树状数组,修改tree[1]+=2 ,tree[2] tree[4] tree[8] … 都会跟着修改。,一直到整个数组的大小N为止。


对于lowbit运算的过程:

lowbit运算可以得到二进制最后一个1的位置,同时也可以根据lowbit确定tree数组的构成

首先我们需要理解tree数组的意思:tree数组本质是前缀和,即tree[i]=a[1]+a[2]+…+a[i]的区间的和。

接下来我们枚举1-7数字的二进制以及lowbit运算的结果,看看lowbit运算是如何影响tree数组的

在这里插入图片描述
首先我们可以了解到:

  • 数字i为奇数时,lowbit始终结果为1,因此我们就可以推断这个1代表tree[i]=a[i] ,代表它只有一个原数组a[i]的值
  • 数字i为偶数时,lowbit结果似乎从数字2,4来看满足:2的时候有两项,4的时候有四项吗,但是并不是这样!!!!!!比如数字6就只有2项。当i为偶数的时候,实际上看的是二进制的最后一位数字1及其之后所表示的低位的值,例如 10就是2;100就是4,110就是2(10);11110还是2(最后一位1与后面的0);11000就是8

因此:tree数组的每一项的值:

  1. 当数字为1时,lowbit运算为1,所以tree[1]=a[1] (奇数肯定只有一项)
  2. 当数字为2时,lowbit运算为10,所以tree[2]=a[2]+a[1],有两项
  3. 当数字为3时,lowbit运算为1,所以tree[3]=a[3]
  4. 当数字为4时,lowbit运算为100,所以tree[4]=a[4]+a[3]+a[2]+a[1] ,有四项
  5. 当数字为5时,lowbit运算为1,所以tree[5]=a[5]
  6. 当数字为6时,lowbit运算为10,所以tree[6]=a[6]+a[5] ,有两项
  7. 当数字为7时,lowbit运算为1,所以tree[7]=a[7]
  8. 当数字为8时,lowbit运算为1000,所以tree[8]=a[8]+ … a[1],有八项

即我们可以得到这样一张图:

请仔细理解这张图代表的意义!!!!!!!!!
在这里插入图片描述


单点修改,区间查询

#include <bits/stdc++.h>
using namespace std;

/*
树状数组:常用于对动态区间的查询操作,即求动态数组求前缀和的操作
三大基本功能:
1. 维护 update
2. 求和 sum
3. lowbit运算
*/
const int N = 10005;
int tree[N]{
     0 };
int n;		//当前tree数组的最大元素个数
inline int lowbit(int x)
{
    
	return ((x) & (-x));
}

//维护
void update(int i,int x)
{
    
	/*
	i:下标
	x:增量
	维护:在末尾的1上加1
	*/
	while (i <= n)
	{
    
		tree[i] += x;
		i += lowbit(i);
	}
}

//求和
int sum(int i)
{
    
	/*
	求和(查询):去除末尾的1
	*/
	int num = 0;
	while (i)
	{
    
		num += tree[i];
		i -= lowbit(i);
	}
	return num;
}
int main()
{
    
	n = 5;
	for (int i = 1; i <= 5; i++)
	{
    
		update(i, 3);
	}
	// 3 6 9 12 15
	cout << "S[2,4]: " << sum(4) - sum(1) << endl;

	//更新数组元素
	// 3 6 15 18 21
	update(3, 6);

	cout << "S[2,4]: " << sum(4) - sum(1) << endl;
	return 0;
}

区间修改,单点查询

在树状数组中可以用前 i 项的和来表示第 i 个数.

差分原理:a数组是一个原始数组,b数组是差分数组
a数组: 1,2,3,4,5
b数组: 1,1,1,1,1

所以可以得到:b[i]=a[i] - a[i-1]

通过差分数组b可以得到a数组的元素值:

a[4]=b[1]+b[2]+b[3]+b[4](易得)

对a数组的 2到4区间的元素都加 3:
a数组:1,5,6,7,5

不妨把b数组的2位置与4+1位置的元素加3和减3,其他位置元素不变
b数组:1,4,1,1,-2

则a[4]= b[1]+b[2]+b[3]+b[4]= 7 仍然等于对a数组的值。

因此对a数组的[x,y]区间操作,其实就是对 b差分数组执行两个位置的操作即可:

  • b[x]+=k
  • b[y+1]-=k

那么当对 x ~ y 的区间进行修改的时候需要在树状数组中的第 x 个位置 + k, 第 y + 1 个位置 -k

#include <bits/stdc++.h>
using namespace std;
#define int long long
const int N=5e5+10;
int n,m;
int nums[N],tree[N];
inline int lowbit(int i){
    
	return ((i)&(-i));
}
void update(int i,int num){
    	
	while (i<=n){
    
		tree[i]+=num; 
		i+=lowbit(i);
	}
}
int query(int i){
    
	int ans=0;
	while (i){
    
		ans+=tree[i];
		i-=lowbit(i);
	}
	return ans;
}
signed main(){
    
	cin>>n>>m;
	for (int i=1;i<=n;i++){
    
		cin>>nums[i];
		update(i,nums[i]-nums[i-1]); //维护差分值 
	} 
	while (m--){
    
		int a,b,c,d;
		cin>>a;
		if (a==1){
    
			//[b,c]+d
			cin>>b>>c>>d;
			update(b,d);      //b[i]+=d
			update(c+1,-d);   //d[i+1]-=d
		}
		else if (a==2){
    
			//query b
			cin>>b;
			cout<<query(b)<<'\n';
		}
	}
	return 0;
}
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/jj6666djdbbd/article/details/128758445

智能推荐

element-ui中el-pagination分页组件,切换每页显示数量时,有时候数据不显示或获取到的数据为空;只想触发size-change事件不触发current-change事件_element-ui 分页展示数据 不全-程序员宅基地

文章浏览阅读1.3w次,点赞9次,收藏20次。element-ui中el-pagination分页组件使用注意事项切换每页显示数量时,有时候数据不显示或获取到的数据为空el-pagination组件只想触发size-change事件不触发current-change事件_element-ui 分页展示数据 不全

嵌入式面试题 C C++ 队列面试题总结-程序员宅基地

文章浏览阅读864次,点赞19次,收藏20次。你的支持,我的动力;祝各位前程似锦,offer不断,步步高升!!!” />你的支持,我的动力;祝各位前程似锦,offer不断,步步高升!!!更多资料点击此处获qu!!

Android热修复原理热修复框架对比和代码修复-程序员宅基地

文章浏览阅读750次,点赞26次,收藏20次。在Android应用开发中,热修复技术被越来越多的开发者所使用,也出现了很多热修复框架,比如:AndFix、Tinker、Dexposed和Nuwa等等。如果只是会这些热修复框架的使用那意义并不大,我们还需要了解它们的原理,这样不管热修复框架如何变化,只要基本原理不变,我们就可以很快的掌握它们。这一个系列不会对某些热修复框架源码进行解析,而是讲解热修复框架的通用原理。

SpringMVC文件上传功能总结_spring mvc文件上传总结-程序员宅基地

文章浏览阅读174次。Springmvc文件上传功能demo,不多说直接上代码dispatcherServlet-servlet.xml中bean配置&lt;bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver"&gt; &lt;property name=..._spring mvc文件上传总结

Sql优化总结!详细!(2021最新面试必问)-程序员宅基地

文章浏览阅读10w+次,点赞625次,收藏5.8k次。Sql优化基础Sql优化查询SQL尽量不要使用select *,而是具体字段避免在where子句中使用or来连接条件使用varchar代替char尽量使用数值替代字符串类型查询尽量避免返回大量数据使用explain分析你SQL执行计划是否使用了索引及其扫描类型创建name字段的索引优化like语句:字符串怪现象索引不宜太多,一般5个以内索引不适合建在有大量重复数据的字段上where限定查询的数据避免在索引列上使用内置函数避免在where中对字段进行表达式操作避免在where子句中使用!=或<>操_sql优化

5G网络用户面时延测量_5g用户面时延-程序员宅基地

文章浏览阅读1.9k次。虽然可以用各分段时延相加获得大致测量 5G 上行或者下行的空口单向时延, 但是由于各分段时延是已经统计平均的,所以没有办法进行 空口单向时延的可靠性测量,且得到的还只是一个大致的单向时延统计而非精确的测量结果(因为上下行都含了部分往返时延)。虽然说5G实现了用户面和控制面的解偶,但是在实际的数据传输过程中,一个数据包从用户发出到到达基站,会有用户面的时延也会有控制面的时延吧,也就是说用户面时延和控制面时延是纠缠在一起的吧。而在3GPP TS38.314中定义的RAN侧下行分组时延包括下图红线的四个部分。.._5g用户面时延

随便推点

SM2259XT2量产工具下载方法,SM2259XT2硬盘开卡修复满血复活,SM2259XT2固件,SM2259XT2量产图文教程_sm2258xt量产工具下载-程序员宅基地

文章浏览阅读482次,点赞7次,收藏4次。​我这个固态是SM2259XT2和海力士HY3D-V7的搭配,就得从量产部落找这个组合的量产工具。_sm2258xt量产工具下载

【BSV无限可能】区块链的商业应用_bsv 跨链能力-程序员宅基地

文章浏览阅读491次。2023年2月在斯洛文尼亚的一个专属会场,nChain首席科学家Craig S. Wright博士举办了自己新一期的比特币大师班。比特币大师班课程是月度系列活动,旨在帮助参会者学习比特币的基本原理及其背后的技术知识。_bsv 跨链能力

2019校招前端笔试面试题_javaee为什么前端某个数据状态发生变化只发生在用户本地,不会影响到其他用户-程序员宅基地

文章浏览阅读1.6w次,点赞38次,收藏261次。前期概要:01你做的页面在哪些流览器测试过?这些浏览器的内核分别是什么?答案IE:trident内核Firefox:gecko内核Safari:webkit内核Opera:以前是presto内核,Opera现已改用Google Chrome的Blink内核Chrome:Blink(基于webkit,Google与Opera Software共同开发)02Javas..._javaee为什么前端某个数据状态发生变化只发生在用户本地,不会影响到其他用户

无线通信中的TD系统(TD-LTE)-程序员宅基地

文章浏览阅读850次。5G中的核心技术_td系统

Linux系统中最佳开源电子邮件服务器_linux邮件服务器有哪些-程序员宅基地

文章浏览阅读1.1k次,点赞27次,收藏20次。以上是Linux系统中备受推荐的开源电子邮件服务器,包括了Postfix、Exim、Dovecot、OpenSMTPD、Mailcow和iRedMail。每种邮件服务器都有其独特的特点和优势,适用于不同的部署场景和需求。希望本文的介绍能够帮助大家选择适合的最佳开源邮件服务器,并建立稳定、安全的邮件系统。更多Python学习内容:ipengtao.com点击下方“阅读原文”查看更多。_linux邮件服务器有哪些

a-select-程序员宅基地

文章浏览阅读712次。a-select支持搜索_a-select

推荐文章

热门文章

相关标签