详解树状数组(C/C++)

news2024/11/11 6:55:58

树状数组(Binary Indexed Tree,简称BIT或Fenwick Tree)是一种用于高效处理数据序列的算法数据结构。它能够支持两个主要操作:单点更新和区间求和,这两个操作的时间复杂度都能达到O(log n),其中 n 是数据序列的长度。树状数组非常适合处理那些需要频繁更新和查询区间和的问题。

基本原理

树状数组的核心思想是将数据序列映射到一棵二叉树中,这棵树并不是普通的二叉树,而是一棵完全二叉树,并且每个节点的值表示从该节点到叶子节点的区间和。通过这棵二叉树,我们可以快速地计算出任意区间的和。

树状数组由名字可知,它是一个树状结构,在点更新操作时,叶子节点的更新导致父亲节点的更新,从而带动整棵树的更新,它的结构是一棵树,树状的数组,它的值类似于前缀和的思想,每一个lowbit(i)都管着前面所有原数组的值,在进行更新或者计算时可以大大减少操作,从而做到减少时间复杂度的目的。

特点

1. 高效性:树状数组可以在O(log n)的时间复杂度完成点更新和区间求和,普通点更新和区间求和都需要O(n),大大提升了效率。

2. 空间优化:相比于线段树,树状数组的空间复杂度更低,只需要一个大小为 n+1 的数组,并且树状数组的实现比线段树简单非常多。

3.树状数组的下标必须从1开始,不能从0开始。


核心操作

1. 单点更新:将单个点的值修改为num。
2. 区间求和:将数组第 l 个元素到第 r 个元素进行求和。

算法实现

下面将以C语言为例进行算法实现,lowbit函数会求出二进制数字的最低位代表哪个数字,例如10110,最低位为1的是2。

单点更新: 

add函数是对第x点增加k,此时我们就要更新其所有父亲节点,也就是每一步的lowbit(i),使其所有管着它的父亲节点都增加k。

区间求和:

query函数是区间求和,求[1,x]范围内的和,如果求[n,m]范围内可以采用前缀和的思想实现,即query(m)-query(n-1)。 

#include<stdio.h>
int a[100005];
int c[100005];
int n,m;
int sum;
int lowbit(int x){//c[i]的区间长度,就是管着几个a[i]
	return x&(-x);
}
void add(int x,int k){//c[x]父子树更新加上k,a[x]加上k,点更新
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=k;
	}
}
int query(int x){//求区间和1--x
	int s=0;
	for(int i=x;i>0;i-=lowbit(i)){
		s+=c[i];
	}
	return s;
}
int main(){
	int i,j,x,y;
	scanf("%d%d",&n,&m);
	scanf("%d%d",&x,&y);
	for(i=1;i<=n;i++){//树状数组的下表必须从1开始
		scanf("%d",&a[i]);
	}
	for(i=1;i<=n;i++){
		add(i,a[i]);
	}
	add(m,m);//对第m个数改变m
	printf("%d",query(y)-query(x-1));//求x--y区间的和
	return 0;
}

视频辅助讲解可以看一下这个动画讲解,非常形象-->点击直达<-- 


树状数组应用

树状数组在算法竞赛和实际应用中非常常见,主要有以下操作例如:

1. 求逆序对数量:

逆序对为前面的数比后面的数大,例如:【3, 1】这就是一对逆序对,【4,2,1,3】此序列有3对逆序对分别为【4,2】、【4,1】、【4,3】、【2,1】。

那么我们如何通过树状数组求逆序对的数量呢。首先我们初始化一个都为0的树状数组,把原数组进行离散化,保存下标pos到结构体之中,把原数组中的数据按照降序的顺序排序。此时离散化的下标就打乱了顺序。从头到尾遍历每一个位置,求它前一个位置的区间和就是此数与前面的数能够构成逆序对的数量,每遍历完一个,点更新一次,这样就对应了每遍历一次就进行一次区间求和、单点更新。

图解算法:

我们以【4,2,1,3】为例进行每一步模拟。

 树状数组求逆序对的视频讲解可以看一下董晓老师的讲解:C83 树状数组 P1908 逆序对_哔哩哔哩_bilibili


代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=5e5+5;
struct node{//val值pos位置
	int val,pos;
}a[N];
int c[N];
int n;
ll ans;
int lowbit(int x){//c[i]的区间长度,就是管着几个a[i]
	return x&(-x);
}
bool cmp(node A,node B){
	if(A.val==B.val){
		return A.pos>B.pos;
	}
	return A.val>B.val;
}
void add(int x,int k){//c[x]父子树更新加上k,a[x]加上k,点更新
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=k;
	}
}
int query(int x){//求区间和1--x
	int s=0;
	for(int i=x;i>0;i-=lowbit(i)){
		s+=c[i];
	}
	return s;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i].val;
		a[i].pos=i;
	}
	sort(a+1,a+n+1,cmp);//降序排序
	for(int i=1;i<=n;i++){//遍历每个位置
		ans+=query(a[i].pos-1);//求它前一个位置的和---区间求和
		add(a[i].pos,1);//单点修改
	}
	cout<<ans<<endl;
	return 0;
}

 2. 区间修改,单点查询:

区间修改,单点查询与前面树状数组核心操作恰好相反,前面的树状数组都是前缀和的思想,那么将前缀和反过来就是差分,可以通过差分来实现区间修改与单点查询。

差分数组是这样定义的c[i]=a[i]-a[i-1](1<i<=n),特殊情况在端点处c[1]=a[1],c[n]=-a[n-1],实现区间修改时例如在[l,r]区间+d操作,转换为差分数组c[l]+d,c[r+1]-d。当需要单点查询时,我们可以把差分数组利用前缀和的思想给还原回去,a[i]=c[i]+a[i-1]等价于1—i对差分数组进行求和。

代码实现:
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=5e5+5;
int a[N],c[N];
int n;
ll ans;
int lowbit(int x){
	return x&(-x);
}
void add(int x,int k){
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=k;
	}
}
int query(int x){//求区间和1--x
	int s=0;
	for(int i=x;i>0;i-=lowbit(i)){
		s+=c[i];
	}
	return s;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		add(i,a[i]-a[i-1]);
	}
	//区间更新[l,r]上+k
	int l,r,k;
	cin>>l>>r>>k;
	add(l,k);
	add(r+1,-k);
	for(int i=1;i<=n;i++){
		cout<<query(i)<<" ";
	}
	cout<<endl;
	//查询第x点的值
	int x;
	cin>>x;
	cout<<query(x)<<endl;
	return 0;
}

3.TOP K问题(区间第K大问题):

这类问题我们可以利用树状数组的思想,可以在O(nlogn)的时间内找到一个数组中第K大的元素。

主要步骤:
  1. 构建树状数组:首先,创建一个大小为n的树状数组,并将数组的初始值设为0。然后,将原始数组中的每个元素依次插入树状数组中,相当于进行了n次更新操作。

  2. 预处理树状数组:在构建树状数组的过程中,对于每个插入的元素,需要更新树状数组中对应位置的值。具体操作是将该位置上的值增加1。

  3. 查询第K大的元素:从大到小遍历原始数组中的元素,并从树状数组中查询对应位置的值。假设当前遍历的元素是a[i],则查询树状数组中小于等于a[i]的元素数量。如果这个数量大于等于K,说明a[i]是第K大的元素;否则,将K减去这个数量,继续遍历下一个元素。

代码实现:
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N=2e5+5;
int n,k;
int c[N];

int lowbit(int x){
	return x&(-x);
}
void add(int x,int k){
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=k;
	}
}
int query(int x){
	int s=0;
	for(int i=x;i>0;i-=lowbit(i)){
		s+=c[i];
	}
	return s;
}
// 查询第K大的元素
int find_top_k(vector<int>& nums, int k) {
    // 离散化处理
    vector<int> sortedNums(nums);
    sort(sortedNums.begin(), sortedNums.end());
    for (int i = 0; i < n; i++) {
        nums[i] = lower_bound(sortedNums.begin(), sortedNums.end(), nums[i]) - sortedNums.begin() + 1;
    }
    // 更新树状数组
    for (int i = 0; i < n; i++) {
        add(nums[i], 1);
    }
    // 二分查找
    int left = 1, right = n;
    while (left < right) {
        int mid = (left + right) / 2;
        int count = query(mid);
        if (count >= k) {
            right = mid;
        } else {
            left = mid + 1;
        }
    }
    return sortedNums[left-1];
}

int main() {
    cin>>n;
    vector<int> nums(n);
    for(int i=0;i<n;i++){
    	cin>>nums[i];
	}
    cin>>k;
    cout << find_top_k(nums, k) << endl;
    return 0;
}

算法例题

洛谷 P1908 逆序对

题目描述

猫猫 TOM 和小老鼠 JERRY 最近又较量上了,但是毕竟都是成年人,他们已经不喜欢再玩那种你追我赶的游戏,现在他们喜欢玩统计。

最近,TOM 老猫查阅到一个人类称之为“逆序对”的东西,这东西是这样定义的:对于给定的一段正整数序列,逆序对就是序列中ai​>aj​ 且i<j 的有序对。知道这概念后,他们就比赛谁先算出给定的一段正整数序列中逆序对的数目。注意序列中可能有重复数字。

输入格式

第一行,一个数 n,表示序列中有 n个数。

第二行 n 个数,表示给定的序列。序列中每个数字不超过 10^9。

输出格式

输出序列中逆序对的数目。

输入 

6
5 4 2 6 3 1

输出 

11
说明/提示

对于 25% 的数据,n≤2500

对于 50% 的数据,n≤4×10^4。

对于所有数据,n≤5×10^5


解题思路:

是树状数组求逆序对数量的模板题,直接复制上面的代码。

AC代码:
#include<iostream>
#include<algorithm>
using namespace std;
typedef long long ll;
const int N=5e5+5;
struct node{
	int val,pos;
}a[N];
int c[N];
int n;
ll ans;
int lowbit(int x){//c[i]的区间长度,就是管着几个a[i]
	return x&(-x);
}
bool cmp(node A,node B){
	if(A.val==B.val){
		return A.pos>B.pos;
	}
	return A.val>B.val;
}
void add(int x,int k){//c[x]父子树更新加上k,a[x]加上k,点更新
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=k;
	}
}
int query(int x){//求区间和1--x
	int s=0;
	for(int i=x;i>0;i-=lowbit(i)){
		s+=c[i];
	}
	return s;
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i].val;
		a[i].pos=i;
	}
	sort(a+1,a+n+1,cmp);
	for(int i=1;i<=n;i++){
		ans+=query(a[i].pos-1);
		add(a[i].pos,1);
	}
	cout<<ans<<endl;
	return 0;
}

AcWing 244. 谜一样的牛

有 n 头奶牛,已知它们的身高为 1∼n 且各不相同,但不知道每头奶牛的具体身高。

现在这 n 头奶牛站成一列,已知第 i 头牛前面有 Ai 头牛比它低,求每头奶牛的身高。

输入格式

第 1 行:输入整数 n。

第 2..n 行:每行输入一个整数 Ai,第 i 行表示第 i 头牛前面有 Ai 头牛比它低。
(注意:因为第 1 头牛前面没有牛,所以并没有将它列出)

输出格式

输出包含 n 行,每行输出一个整数表示牛的身高。

第 i 行输出第 i 头牛的身高。

数据范围

1≤n≤10^5

输入样例:
5
1
2
1
0
输出样例:
2
4
5
3
1

解题思路:

这道题博主真的没有想到会用树状数组求解,本题解题方法为树状数组+二分,还是比较考验思维的,这道题的树状数组考察是前面所说的TOP K问题。题目看似很简单,但不知如何下手,这样的问题处理一般是先从边界处理,要么先处理最左边的,要么先处理最右边的。这道题我们从后往前处理,因为题目条件给出了第 i 头牛前面有 Ai 头牛比它低这个条件,这样可以二分出答案,不用考虑已经推出来的数,如果从前往后的话,还要考虑之前已经推出来的数。

由于每头牛的高度各不相同且在[1,n]之内,因此,对于倒数第二头牛而言,它应该在除去最后一头牛的身高,且在区间[1,n]中,选取比a[n−1]+1小的数且最接近的一个。其他的牛以此类推。假如建立一个全部元素为1的身高数列,某个位置的数为1代表这个高度还不知道是哪头牛的,那么就用树状数组维护该数列的前缀和,若某个位置的前缀和等于a[i+1]此时的下标就是要找的数。选择这个数后,将相应位置的1置0,可以二分这个位置。


AC代码:
#include<iostream>
using namespace std;
const int N=2e5+5;
int a[N],c[N],ans[N];//a是原数组c是树状数组ans是结果数组
int n;

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

void add(int x,int k){
	for(int i=x;i<=n;i+=lowbit(i)){
		c[i]+=k;
	}
}
int query(int x){
	int s=0;
	for(int i=x;i>0;i-=lowbit(i)){
		s+=c[i];
	}
	return s;
}
int main(){
	cin>>n;
	add(1,1);//点更新
	for(int i=2;i<=n;i++){
		cin>>a[i];
		add(i,1);
	}
	for(int i=n;i>=1;i--){//倒着先从最后一个往前推
		int l=1,r=n;
		while(l<r){//二分答案,需要找的数
			int mid=l+r>>1;
			if(query(mid)<a[i]+1){
				l=mid+1;
			}else{
				r=mid;
			}
		}
		ans[i]=l;//找到答案赋值
		add(l,-1);//置0,点更新
	}
	for(int i=1;i<=n;i++){
		cout<<ans[i]<<endl;
	}
	return 0;
}

AcWing 1265. 数星星

天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。

本题采用数学上的平面直角坐标系,即 x 轴向右为正方向,y 轴向上为正方向。

如果一个星星的左下方(包含正左和正下)有 k 颗星星,就说这颗星星是 k 级的。

例如,上图中星星 5 是 3 级的(1,2,4在它左下),星星 2,4 是 1 级的。

例图中有 1 个 0 级,2 个 1 级,1 个 2 级,1 个 3 级的星星。

给定星星的位置,输出各级星星的数目。

换句话说,给定 N 个点,定义每个点的等级是在该点左下方(含正左、正下)的点的数目,试统计每个等级有多少个点。

输入格式

第一行一个整数 N,表示星星的数目;

接下来 N行给出每颗星星的坐标,坐标用两个整数 x,y 表示;

不会有星星重叠。星星按 y 坐标增序给出,y 坐标相同的按 x 坐标增序给出。


解题思路、AC代码:

由于文章长度限制,这里不在详解,可以移步我的这一篇博客,专门讲解的这一道题。

AcWing 1265. 数星星(每日一题)_天空中有一些星星,这些星星都在不同的位置,每个星星有个坐标。 本题采用数学上的-CSDN博客


由此篇可见树状数组还是非常重要的,算法的效率也是非常高的,在算法竞赛中比较重要,希望对大家有所帮助,文章有错误的地方,恳请各位大佬指出。执笔至此,感触彼多,全文将至,落笔为终,感谢大家的支持。 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2095313.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

STM32基础篇:SPI片上外设

SPI外设简介 STM32芯片内部集成了SPI片上外设&#xff0c;可由硬件自动执行时钟生成、数据收发等功能&#xff0c;减轻CPU负担。对于STM32F103C8T6&#xff0c;其SPI资源有SPI1、SPI2。 一些参数配置&#xff1a; 8位/16位数据帧高位先行/低位先行时钟频率&#xff1a;PCLK/…

vue3本地运行错误集

1、解决报错ValidationError: Progress Plugin Invalid Options问题 ValidationError: Progress Plugin Invalid Optionsoptions should NOT have additional propertiesoptions should NOT have additional propertiesoptions should NOT have additional propertiesoptions …

SMART PLC 脉冲输出指令PLS应用

200SMART PLC如何实现可调频率可调占空比PWM输出 200smart_PLC如何实现可调频率可调占空比PWM输出_200smart pwm-CSDN博客文章浏览阅读6.4k次,点赞2次,收藏7次。本文介绍了如何在SMART PLC中通过修改原向导接口,实现可调频率和占空比的PWM输出。详细阐述了脉冲周期、占空比…

DMA简述与使用实例

之后要学&#xff1a;SPI / IICDMA 学习的这位up主的视频&#xff1a;全网最清楚的DMA讲解&#xff0c;三种搬运模式三个例子讲清楚&#xff08;STM32教程基于HAL库和CUBEIDE&#xff09;_哔哩哔哩_bilibili 目录 01-基本信息 1-概述 2-方向 3-模式 正常模式 轮询模式 …

学习日志8.30--防火墙NAT

目录 一、实验环境配置 二、配置防火墙静态NAT一对一 三、配置防火墙静态NAT多对多 四、配置防火墙NAT端口转换NAPT 五、防火墙smart-nat、easyip 六、防火墙三元组NAT 在学习过基于路由器的NAT网络地址转换&#xff0c;现在学习基于防火墙NAT的网络地址转换&#xff0c;…

python-读写Excel:xlwings库操作

几种操作Excel的python库对比 安装:pip install xlwings 目录 APP实例化对象 工作薄对象 创建工作薄 打开工作薄 工作薄属性 工作表对象 新增工作表 复制表 获取工作表对象 工作表属性 删除和清除表数据及表格式 工作表行高列宽(自动调整) 单元格对象 获取单元…

【hot100篇-python刷题记录】【旋转图像】

R7-矩阵篇 印象题&#xff1a; 思路&#xff1a; 先转置&#xff0c;转置完我们按照列的中间进行对称交换就可以了。 class Solution:def rotate(self, matrix: List[List[int]]) -> None:"""Do not return anything, modify matrix in-place instead.&qu…

【微机原理】指令JZ和JNZ的区别

&#x1f31f; 嗨&#xff0c;我是命运之光&#xff01; &#x1f30d; 2024&#xff0c;每日百字&#xff0c;记录时光&#xff0c;感谢有你一路同行。 &#x1f680; 携手启航&#xff0c;探索未知&#xff0c;激发潜能&#xff0c;每一步都意义非凡。 JZ&#xff08;Jump …

祝贺严建兵教授任华中农业大学校长

公众号&#xff1a;生信漫谈&#xff0c;获取最新科研信息&#xff01; 祝贺严建兵教授任华中农业大学校长https://mp.weixin.qq.com/s?__bizMzkwNjQyNTUwMw&mid2247487040&idx1&sn6800055c9944754be11dc77a30ee1906&chksmc0e9ebb0f79e62a64634d5cd057578ca5…

Java 入门指南:Java 并发编程 —— AQS、AQLS、AOS 锁与同步器的框架

AQS AQS 是 AbstractQueuedSynchronizer 的缩写&#xff0c;即 抽象队列同步器&#xff0c;是 Java.util.concurrent 中的一个基础工具类&#xff0c;用于实现同步器&#xff08;Synchronizer&#xff09;的开发。 AQS 提供了一种实现锁和同步器的框架&#xff0c;使用 AQS 能…

Mysql高级篇(上)

Mysql高级篇&#xff08;上&#xff09; Mysql架构介绍(一)1、Linux环境下的MySQL的安装与使用2、MySQL请求到响应字符集变化&#xff08;了解&#xff09;3、MySQL8 的主要目录结构4、数据库和文件系统关系&#xff08;1&#xff09;默认数据库&#xff08;2&#xff09;数据库…

C语言程序设计之基础易错题锦集6

C语言程序设计之基础易错题锦集6 问题 6_0解析 6_0 问题 6_0 将形参 s 所指字符串中字母字符顺序前移&#xff0c;其他字符顺序后移&#xff0c;处理后将新字符串的首地址作为函数值返回。 例如&#xff1a;输入 &#xff1a;“asd123fgh456df”, 输出&#xff1a;“a…

React基础面试题

React 面试题 以下是面试官最有可能问到的 50 个 React 面试题和答案。为方便你学习&#xff0c;我对它们进行了分类&#xff1a; 基本知识React 组件React ReduxReact 路由 基本知识 1. 区分Real DOM和Virtual DOM Real DOMVirtual DOM1. 更新缓慢。1. 更新更快。2. 可以…

简化理解:Tomcat 和 Servlet 规范

有时候&#xff0c;我们会把复杂的技术概念弄得很复杂&#xff0c;其实这些东西可以用更简单的语言来理解。我们来看看 Tomcat 和 Servlet 规范到底是怎么回事。 1. 什么是 Servlet 规范&#xff1f; 简单来说&#xff0c;Sun 公司&#xff08;现在是 Oracle&#xff09;定了…

YOLOv9改进策略【模型轻量化】| MoblieNetV3:基于搜索技术和新颖架构设计的轻量型网络模型

一、本文介绍 本文记录的是基于MobileNet V3的YOLOv9目标检测轻量化改进方法研究。MobileNet V3的模型结构是通过网络搜索得来的&#xff0c;其中的基础模块结合了MobileNet V1的深度可分离卷积、MobileNet V2的线性瓶颈和倒置残差结构以及MnasNet中基于挤压和激励的轻量级注意…

注意力机制(Attention mechanism)(上篇)

在图像识别的时候&#xff0c;假设输入的图像大小都是一样的。但如果问题变得复杂&#xff0c;如图1所 示&#xff0c;输入是一组向量&#xff0c;并且输入的向量的数量是会改变的&#xff0c;即每次模型输入的序列长度都不一 样&#xff0c;这个时候应该要怎么处理呢&#xff…

随笔十、音频扩展模块测试

本项测试简单&#xff0c;对购买的音频扩展模块进行录音放音测试 按照使用说明&#xff0c;连接音频小板&#xff0c;一个喇叭一个麦克风&#xff0c;4根线&#xff0c;buildroot系统镜像 录音测试 rootRK356X:/# arecord -c 1 -r 44100 -f S16_LE /tmp/record.wav Recording …

Java-多线程入门

多线程是指在软件或硬件上实现多个线程并发执行的技术。为了更好地理解多线程&#xff0c;首先需要了解几个基本概念&#xff1a; 了解概念 1.程序 程序是为完成特定任务、用某种语言编写的一组指令的集合。它是一个静态的概念&#xff0c;通常存储在磁盘或其他非易失性存储器…

vxe-table 更新到最新版本

当前版本&#xff1a; "vxe-table": "^4.3.0-beta.3" 更新后&#xff1a; "vxe-table": "^4.7.75" 需要调整代码&#xff1a; 更改前main.js 更改后&#xff1a;

Jenkins Environment Injector Plugin 插件详解

引言 在做自动化测试的过程中&#xff0c;我们需要经常发送测试报告给相关研发、产品和上级&#xff0c;但是Jenkins邮件模板不支持Javascritpt脚本来动态生成数据&#xff0c;只支持静态的HTML代码&#xff0c;那么我们就没有办法了吗&#xff1f;非也&#xff0c;我们可以通…