数据结构: 树状数组

2024-08-25 04:12
文章标签 数组 数据结构 树状

本文主要是介绍数据结构: 树状数组,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在OI赛事中,数据结构是非常重要的一个内容,更是有人说过,算法+数据结构=程序:
A l g o r i t h m + D a t a Algorithm+Data Algorithm+Data S t r u c t u r e = P r o g r a m m i n g Structure=Programming Structure=Programming
接下来,我们就来介绍一些经典重要的数据结构。
首先是树状数组。

实际上,数据结构就相当于一种算法,是在某种结构上实现某算法。
树状数组是一个查询和修改复杂度都为 O ( l o g 2 n ) O(log_2 n) O(log2n) 的数据结构,运用树状数组可以实现很多算法问题,

比如单点修改,区间求和,求逆序对等等。
比如区间求和,若 n n n 为数列长度, m m m 为查询次数,那么普通查询总和的最坏时间复杂度为 O ( n m ) O(nm) O(nm),假设 n n n m m m < = <= <= 1 0 5 10^{5} 105,时间复杂度就接受不了了,所以很多数据结构比如树状数组,RMQ问题的ST,线段树以及倍增(不太算是数据结构,偏向于 A l g o r i t h m Algorithm Algorithm)之类的,多数情况下都是为了压缩时间复杂度,尽量达到 O ( 1 0 5 − 1 0 7 ) O(10^5-10^7) O(105107)左右,使程序能在 1 s 1s 1s 的时间内求解。
而树状数组的代码效率远远高于线段树,树状数组代码比较少,但线段树代码量特别大

基本思想:

根据任意正整数关于 2 2 2 的不重复次幂的唯一分解性质,若一个正整数 x x x 的二进制表示为 1010 1 ( 2 ) 10101_{(2)} 10101(2),其中 1 1 1 的位是 0 , 2 , 4 0,2,4 0,2,4 ,则 x x x 可以被“二进制分解”成 2 4 + 2 2 + 2 0 2^{4}+2^{2}+2^{0} 24+22+20,进一步,区间 [ 1 , x ] [1,x] [1,x],可以分成 l o g 2 x log_2 x log2x个小区间:

  1. 长度为 2 4 2^4 24 的小区间 [ 1 , 2 4 ] [1,2^{4}] [1,24]
  2. 长度为 2 2 2^2 22 的小区间 [ 2 4 + 1 , 2 4 + 2 2 ] [2^{4}+1,2^{4}+2^{2}] [24+1,24+22]
  3. 长度为 1 1 1 的小区间 [ 2 4 + 2 2 + 1 , 2 4 + 2 2 + 1 ] [2^{4}+2^{2}+1,2^{4}+2^{2}+1] [24+22+1,24+22+1]

树状数组就是基于上述思想的数据结构,基本作用是维护序列的前缀和。
对于区间 [ 1 , x ] [1,x] [1,x],树状数组将其分为 l o g 2 x log_2 x log2x 个小区间, l o g 2 x log_2 x log2x 个子区间的累加和就是 [ 1 , x ] [1,x] [1,x] 的区间和。

基本算法:

由上文可知,这些子区间的共同特点是,若区间结尾为 R R R,则区间长度就等于 R R R 的“二进制分解”下最小的 2 2 2 的次幂,我们设为 l o w b i t ( R ) lowbit(R) lowbit(R)
l o w b i t lowbit lowbit的计算很简单, l o w b i t ( n ) = n lowbit(n)=n lowbit(n)=n & ( − n ) (-n) (n),表示 n n n在二进制表示下最低位的 1 1 1 以及后面的 0 0 0 构成的数。如 l o w b i t ( 10110 0 ( 2 ) ) = 10 0 ( 2 ) = 4 ( 10 ) lowbit(101100_{(2)})=100_{(2)}=4_{(10)} lowbit(101100(2))=100(2)=4(10)
我们就可以写一个子程序函数 l o w b i t lowbit lowbit 来实现:

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

对于序列 A A A,我们建立一个数组 C C C,其中 C C C [ x ] [x] [x] 保存序列 A A A 的区间 [ x − l o w b i t ( x ) + 1 , x ] [x-lowbit(x)+1,x] [xlowbit(x)+1,x] 中所有数之和。
树状数组

我们构造上图所示的结构,满足以下性质:

  1. C C C [ x ] [x] [x] 等于以 x x x 为根的子树中所有叶节点的和。
  2. C C C [ x ] [x] [x] 的叶节点个数等于 l o w b i t ( x ) lowbit(x) lowbit(x)
  3. C C C [ x ] [x] [x] 的父亲节点是 C C C [ x + l o w b i t ( x ) ] [x+lowbit(x)] [x+lowbit(x)]
  4. 深度为 l o g 2 N log_2 N log2N
    如图我们可以知道:
    C C C [ 1 ] [1] [1] = A [ 1 ] A[1] A[1]
    C C C [ 2 ] [2] [2] = A [ 1 ] A[1] A[1] + A [ 2 ] A[2] A[2]
    C C C [ 3 ] [3] [3] = A [ 3 ] A[3] A[3]
    C C C [ 4 ] [4] [4] = A [ 1 ] A[1] A[1] + A [ 2 ] A[2] A[2] + A [ 3 ] A[3] A[3] + A [ 4 ] A[4] A[4]
    C C C [ 5 ] [5] [5] = A [ 5 ] A[5] A[5]
    C C C [ 6 ] [6] [6] = A [ 5 ] A[5] A[5] + A [ 6 ] A[6] A[6]
    C C C [ 7 ] [7] [7] = A [ 7 ] A[7] A[7]
    C C C [ 8 ] [8] [8] = A [ 1 ] A[1] A[1] + A [ 2 ] A[2] A[2] + A [ 3 ] A[3] A[3] + A [ 4 ] A[4] A[4] + A [ 5 ] A[5] A[5] + A [ 6 ] A[6] A[6] + A [ 7 ] A[7] A[7] + A [ 8 ] A[8] A[8]

对某个单点的元素进行修改

就先讲加法操作
树状数组支持单点加法操作,根据树状数组的结构特性,节点 x x x 及其所有祖先节点的 C C C 值会受到操作的影响,而任意一个节点的祖先最多有 l o g 2 N log_2 N log2N 个,逐一对 C C C 值进行加法就行了。时间复杂度为 O ( l o g 2 N ) O(log_2 N) O(log2N)
Code:

void update(int x,int y) {  //将第x个数加上yfor(;x<=N;x+=lowbit(x))  c[x]+=y;
}

而要将某个单点的元素直接改成 y y y,则需要如下代码:

void update(int x,int y) {  //将第x个数改成yfor(int i=x;i<=N;i+=lowbit(i))  c[i]+=(y-a[i]);
}

查询前缀和

树状数组支持查询前缀和,较为简单,时间复杂度为 O ( l o w 2 N ) O(low_2 N) O(low2N) 直接给出
Code:

long long ask_sum(int x) {  //函数名各式各样//查询前x个数的和long long ans=0;for(;x;x-=lowbit(x))  ans+=c[x];return ans;
}

接下来,我们用一道简单的例题来详细讲解一下树状数组

例题

单点修改区间查询
[题目描述]

已知一个数列,你需要进行下面两种操作:

  1. 将某个数加上 x x x
  2. 求出某区间所有数的和。
输入格式

第一行两个整数 n , m n,m n,m, 分别表示该数列长度和操作个数
第二行 n n n 个整数,表示原数列。
接下来 m m m 行每行包含 3 3 3 个整数,表示一个操作,具体如下:
第一个整数为opt,有下列两种输入情况
1 x k: 将第 x x x 个数加上 k k k
2 x y: 询问区间 [ x , y ] [x,y] [x,y] 所有数的和
输出格式
对于每个opt=2,输出一行一个整数,表示相应的结果。

数据范围

1 1 1 < = <= <= n , m n,m n,m < = <= <= 1 0 6 , 10^{6}, 106, 0 0 0 < = <= <= ∣ a [ i ] ∣ , x |a[i]|,x a[i],x < = <= <= 1 0 6 10^{6} 106

树状数组模板题,只是操作2不是求前缀和,是某个区间 [ x , y ] [x,y] [x,y] 的和,我们用 a s k _ s u m ( y ) − a s k _ s u m ( x − 1 ) ask \_ sum(y)-ask \_ sum(x-1) ask_sum(y)ask_sum(x1) 就可以了。
Code:

#include<bits/stdc++.h>
int n,q,opt;
long long a[10000001],c[10000001];
long long x,y;
inline int lowbit(int x) {  //lowbit函数return x&(-x);
}
inline void update(int x,long long k) {for(;x<=n;x+=lowbit(x))  c[x]+=k;
}
inline long long ask_sum(int x) {long long ans=0;for(;x;x-=lowbit(x))  ans+=c[x];return ans;
}
int main() {std::cin>>n>>q;for(int i=1;i<=n;i++)  scanf("%lld",a+i),update(i,a[i]);  //输入while(q--) {scanf("%d %lld %lld",&opt,&x,&y);switch(opt) {case 1:  //当opt=1时将某个单点元素加上yupdate(x,y); break;case 2:  //当等于2时查询某个区间所有数的和printf("%lld\n",ask_sum((int)y)-ask_sum((int)x-1)); break;}}
}

树状数组的模板是非常重要的,虽然说理解也很重要,但在背诵的同时理解的效率会更高。
这篇介绍树状数组的文章到此结束了,如有bug请指出,不胜感激!
下一篇数据结构的文章介绍线段树

这篇关于数据结构: 树状数组的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1104506

相关文章

hdu2241(二分+合并数组)

题意:判断是否存在a+b+c = x,a,b,c分别属于集合A,B,C 如果用暴力会超时,所以这里用到了数组合并,将b,c数组合并成d,d数组存的是b,c数组元素的和,然后对d数组进行二分就可以了 代码如下(附注释): #include<iostream>#include<algorithm>#include<cstring>#include<stack>#include<que

【数据结构】——原来排序算法搞懂这些就行,轻松拿捏

前言:快速排序的实现最重要的是找基准值,下面让我们来了解如何实现找基准值 基准值的注释:在快排的过程中,每一次我们要取一个元素作为枢纽值,以这个数字来将序列划分为两部分。 在此我们采用三数取中法,也就是取左端、中间、右端三个数,然后进行排序,将中间数作为枢纽值。 快速排序实现主框架: //快速排序 void QuickSort(int* arr, int left, int rig

hdu 1166 敌兵布阵(树状数组 or 线段树)

题意是求一个线段的和,在线段上可以进行加减的修改。 树状数组的模板题。 代码: #include <stdio.h>#include <string.h>const int maxn = 50000 + 1;int c[maxn];int n;int lowbit(int x){return x & -x;}void add(int x, int num){while

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

《数据结构(C语言版)第二版》第八章-排序(8.3-交换排序、8.4-选择排序)

8.3 交换排序 8.3.1 冒泡排序 【算法特点】 (1) 稳定排序。 (2) 可用于链式存储结构。 (3) 移动记录次数较多,算法平均时间性能比直接插入排序差。当初始记录无序,n较大时, 此算法不宜采用。 #include <stdio.h>#include <stdlib.h>#define MAXSIZE 26typedef int KeyType;typedef char In

C语言:柔性数组

数组定义 柔性数组 err int arr[0] = {0}; // ERROR 柔性数组 // 常见struct Test{int len;char arr[1024];} // 柔性数组struct Test{int len;char arr[0];}struct Test *t;t = malloc(sizeof(Test) + 11);strcpy(t->arr,

C 语言基础之数组

文章目录 什么是数组数组变量的声明多维数组 什么是数组 数组,顾名思义,就是一组数。 假如班上有 30 个同学,让你编程统计每个人的分数,求最高分、最低分、平均分等。如果不知道数组,你只能这样写代码: int ZhangSan_score = 95;int LiSi_score = 90;......int LiuDong_score = 100;int Zhou

计算数组的斜率,偏移,R2

模拟Excel中的R2的计算。         public bool fnCheckRear_R2(List<double[]> lRear, int iMinRear, int iMaxRear, ref double dR2)         {             bool bResult = true;             int n = 0;             dou

C# double[] 和Matlab数组MWArray[]转换

C# double[] 转换成MWArray[], 直接赋值就行             MWNumericArray[] ma = new MWNumericArray[4];             double[] dT = new double[] { 0 };             double[] dT1 = new double[] { 0,2 };

【408数据结构】散列 (哈希)知识点集合复习考点题目

苏泽  “弃工从研”的路上很孤独,于是我记下了些许笔记相伴,希望能够帮助到大家    知识点 1. 散列查找 散列查找是一种高效的查找方法,它通过散列函数将关键字映射到数组的一个位置,从而实现快速查找。这种方法的时间复杂度平均为(