ps:本文需要读者对递归和回溯,位运算的补码,位与操作以及前缀和有一定了解。后面在写到的时候会做简单介绍。

结构介绍:

树状数组是一个查询和修改复杂度都为log(n)的数据结构。主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值。

线段树是一个二叉搜索树,它将一个区间划分成一些单元区间,每个单元区间对应线段树中的一个叶结点。使用线段树可以快速的查找某一个节点在若干条线段中出现的次数,时间复杂度为O(logN)。而未优化的空间复杂度为2N,实际应用时一般还要开4N的数组以免越界,因此有时需要离散化让空间压缩。

树状数组可以理解为线段树的一个子集,能用树状数组解决的问题也可以用线段树解决。由于树状数组涉及位运算,可能不是很好理解。但线段树的编码难度高,占用空间也更大。通常情况下能使用树状数组解决的问题就不使用线段树。下面通过一个问题来实现以上两种数据结构。

动态求连续区间和 (题目来自AcWing:1264)

给定 n 个数组成的一个数列,规定有两种操作,一是修改某个元素,二是求子数列 [a,b]的连续和。

输入格式:
第一行包含两个整数 n 和 m,分别表示数的个数和操作次数。
第二行包含 n 个整数,表示完整数列。
接下来 m 行,每行包含三个整数 k,a,b (k=0,表示求子数列[a,b]的和;k=1,表示第 a 个数加 b)。
数列从 1 开始计数。

输出格式
输出若干行数字,表示 k=0 时,对应的子数列 [a,b] 的连续和。

数据范围
1≤n≤100000,
1≤m≤100000,
1≤a≤b≤n,数据保证在任何时候,数列中所有元素之和均在 int 范围内。

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
1 1 5
0 1 3
0 4 8
1 7 5
0 4 8

输出样例:

11
30
35

本题暴力解法:直接修改元素值并以的时间复杂度求区间和会TLE(超时)。

下面介绍三种解法:

时空复杂度中,M是查询的次数,N是数组元素个数,不考虑建树的时间复杂度。
均考虑最坏情况且不考虑题目所给的必要的数组空间。

1. 分块

通常情况下,我们将数组分成根号N个区块。并预处理出每个区间的元素总和。如图:


单点修改:根据给定的下标直接对数组元素修改,并更新对应的sum值。如图:

区间查询:当查询区间正好位于区块边界时,只需要对将边界内的每块sum求和即可。当不在区块边界时,需要暴力计算边界部分的值,再与内部分块的sum求和。如图:

代码实现:
const int MAXN = 1e6 + 10;//数组空间
int N,M;
int arr[MAXN];//原数组
int sum[MAXN];//分块后每块的总和Sum
int blockSize;//块的大小void add(int idx,int v){sum[idx / blockSize] += v;//块内总和arr[idx] += v;//数组元素
}int query(int left,int right){//left位于第blockIndex1块内的第index1个元素,right位于第blockIndex2块内的第index2个元素int blockIndex1 = left / blockSize, index1 = left % blockSize, blockIndex2 = right / blockSize, index2 = right % blockSize;if (blockIndex1 == blockIndex2) { //区间[left, right]在同一块中int res = 0;for(int i = blockIndex1 * blockSize + index1;i <=  blockIndex1 * blockSize + index2;i++)res += arr[i];return res;}int sum1 = 0,sum2 = 0,sum3 = 0;//区间[left, right]不在同一块中,分别计算三部分//左侧不完整块求和for(int i = blockIndex1 * blockSize + index1;i < blockIndex1 * blockSize + blockSize;i++)sum1 += arr[i];//右侧不完整块求和for(int i = blockIndex2 * blockSize;i <= blockIndex2 * blockSize + index2;i++)sum2 += arr[i];//中间若干个Sum块求和for(int i = blockIndex1 + 1;i < blockIndex2;i++)sum3 += sum[i];return sum1 + sum2 + sum3;
}int main(){cin >> N >> M;blockSize = sqrt(N);for(int i = 1;i <= N;i++){//下标i对应的块下标为i/blockSizecin >> arr[i];sum[i / blockSize] += arr[i];}while(M--){int k,a,b;cin >> k >> a >> b;if(k == 0){cout << query(a,b) << endl;}else add(a,b);}
}
2. 线段树(Segment Tree)

线段树是一种基于二分的分治。每个区间维护自己的左右边界和区间总和(也可以是最大值,最小值等。本题用来维护L和R区间内的数据总和)。假设当前节点维护的区间为[L,R],设mid = (L + R)/2,他的左孩子维护[L,mid],右孩子维护[mid+1,R]。
线段树的结构如下(图中的数值是数组下标,这里令其数值等于下标。为了方便计算,我们会在原数组的首部在多加一个0,使得下标从1开始,方便找左右孩子,后面会讲到):

单点修改:对于线段树的每一个节点(除了叶子节点)而言,他的sum都是由左子树的节点和右子树的节点的sum求和得来,当对某个节点进行修改时,应该对包含他的所有区间都进行修改,并且应该从叶子节点开始修改。
假设当前要修改的下标为idx,我们要从根节点开始,递归寻找他的孩子节点,直到找到叶子节点,该叶子节点的区间为[idx,idx]。然后自底向上更新他和他父节点的sum。如图:

区间查询:从根节点开始,递归查询[L,R]区间。分以下几种情况讨论
[L,R]完全覆盖了当前节点所在区间,立即回溯。将该节点值sum加入总和。

[L,R]与当前节点没有交集,立即回溯。不做任何操作。

[L,R]与当前节点有部分交集,节点内的左子节点在区间内,递归左孩子。

[L,R]与当前节点有部分交集,节点内的右子节点在区间内,递归右孩子。

对于情况1,我们称该节点为完整节点。情况3,4的节点称为部分节点。

如图:[1,2,3]是不完整节点,[4,5,6]是完整节点

单点修改:从上到下查找叶子节点,时间复杂度logn。自底向上更新Sum,时间复杂度logn。

区间查询:由于区间的连续性,我们有:

在线段树的每一层内,部分节点最多只有2个,而且与[L,R]交在两端。

完整节点最多有 2 个, 因为完整节点的兄弟一定不是完整节点,否则它们的父亲也是完整节点,矛盾! 换言之,对于完整节点 u 和 u 的兄弟 v ,若 v 被访问到,则 v 必为部分节点,若 v 未被访问,则 u 必在区间 [L,R] 的某一端。

所以每一层最多访问四个节点,而线段树有logn层,所以复杂度为logn。

代码实现:

这里引入一个额外知识点:本题的区间节点不使用链式存储,使用顺序存储。利用下标间的映射关系找到当前节点的左右孩子。当前下标为x,左孩子下标为2x,右孩子下标为2x + 1。
这也是使用以1作为第一个数下标的原因,如果首位下标为0则无法找到左右孩子。

const int MAXN = 1e6 + 10;//数组空间
struct Node {int l, r, sum;//节点内存储左下标,右下标以及维护的SumNode() {l = 0; r = 0; sum = 0;}Node(int a, int b, int c) {l = a; r = b; sum = c;}
}tr[4*MAXN];
int N, M;
int arr[MAXN];//原数组//pushup 求x这个节点的Sum=左孩子的Sum+右孩子的Sum
int pushup(int x) {tr[x].sum = tr[2 * x].sum + tr[2 * x+1].sum;
}//build 建立l ~ r 的线段树
void build(int x, int l, int r) {//如果l==r 说明这个区间已经不能再分(长度为1)//直接给它赋值if (l == r) {tr[x] = Node(l, r, arr[r]);}else {//不是叶子节点 创建节点 Sum则在回溯时赋值tr[x] = Node(l,r,0);int mid = l + r >> 1;//分别递归左孩子节点 右孩子节点build(2 * x, l, mid);build(2 * x + 1, mid + 1, r);//递归结束后回溯算出 两个子节点的Sum总和 作为父节点的Sumpushup(x);}
}//query 给定根节点x 查询l~r这个子区间的和 l,r是目标区间 tr[x]是当前节点
int query(int x, int l, int r) {//如果序列完全包含这个子区间 则直接加上if (l <= tr[x].l && tr[x].r <= r)return tr[x].sum;int mid = tr[x].l + tr[x].r >> 1;int sum = 0;//不完全包含则递归找下去//如果和左孩子有交集 递归左边if (l <= mid)sum += query(2 * x, l, r);//如果和右孩子有交集 则递归右边if (r >= mid + 1)sum += query(2 * x + 1, l, r);return sum;
}//add 给定根节点x 将下标idx的值加上v
void add(int x, int idx, int v) {//如果他是一个叶子节点//则直接加上这个值if (tr[x].l == tr[x].r)tr[x].sum += v;else {int mid = tr[x].l + tr[x].r >> 1;//如果这个数的位置在他的左边//递归修改他的左孩子if (idx <= mid)add(x * 2, idx, v);//如果这个数的位置在他的右边//递归修改他的右孩子else add(x * 2 + 1, idx, v);pushup(x);//修改完成 自底向上更新Sum}
}int main()
{cin >> N >> M;for (int i = 1; i <= N; i++)cin >> arr[i];//建立线段树build(1, 1, N);while (M--) {int k,a,b;cin >> k >> a >> b;if (k == 0)//输入 根节点 区间左端点 区间右端点//求出[l,r]区间和cout << query(1, a, b) << endl;//输入根节点 位置 a 加上belse add(1, a, b);}return 0;
}
3. 树状数组(Binary Indexed Tree / FenWick Tree)

前置知识点1:lowbit()

这个函数用来返回最低位的1所对应的值,这并不是一个库函数,需要我们自己实现。
例如现在有一个数18,他的二进制表示为(10010)。我们需要求出的是2,即(10)。
两种方法:

  1. 将最后一位的1消掉,再用原数减去消掉1之后的数。
int lowbit(int x){return x - (x & (x - 1));
}
  1. 利用负整数的补码特性,将它与原数做按位与操作。
int lowbit(int x){return x&-x;
}

第二种解法更为精妙且简短,所以我们一般使用第二种方法。

前置知识点2:前缀和和区间和

假设当前有一个数组 nums [1,7,2,6,9,8]
如果想要快速求出[L,R]区间的元素和,直接计算的复杂度是O(n)

我们可以预处理出某个前缀对应的元素和,有如下递推公式:

则nums的前缀和数组add为[0,1,8,10,16,25,33],add[i]表示前i项的和为add[i]。
易得[L,R]区间的元素和为add[R + 1] - add[L]。
例:利用上面的前缀和数组,假设L = 2,R = 4
区间和:sum[L,R] = 2 + 6 + 9 = add[5] - add[2] = 17。

以上是前缀和与区间和的介绍,在本题中,为了方便计算,我们令下标以1开始,设f(a,b)是闭区间[a,b]的元素和(a和b是第a/b个元素,而非下标),那么有:

树状数组

树状数组是基于前缀和+二进制拆分+倍增的数据结构。可能不是很好理解,下面画图介绍。

从图中可以看出,每个大区间都以倍增额的方式被分割成了若干小区间,例如长度为8的区间,被分成了4,2,1的子区间(最后一个8是父节点)。
例:我们想求出[1,5]的区间和,只需求出子区间并累加即可(后面讲解找点的过程)。

下面使用F(a,b)的右边界作为节点。左边界则是子节点的最小值(下标)。节点值将以二进制形式表示,这也是二进制索引树名字的由来。

本图和上图的原理完全相同,每个节点并不是表示一个值,而是一个范围。
例如(110)表示的是区间[101,110]。

那么他的巧妙之处在哪呢?又为何要这样分?
首先,对于[1,8]。我们只需要8个节点,每个节点存储区间和Sum,即O(n)的空间复杂度。
对于每一个子节点,通过一种映射关系找到他的父节点就是问题所在。这时就需要用到前面提到的lowbit函数。对于(101),我们将它加上lowbit(101)就得到了他的父节点(110)。对于叶子节点来说,只需通过下标就能找到他,并通过lowbit函数逐级找到父节点。

单点修改:每个节点维护了他自己最小叶子为左区间到以自己为右区间的总和。修改节点idx后,通过lowbit找到所有祖先节点并更新祖先节点的Sum值。(类似于下级上报给上级)。

这就是前面提到的找点的过程,通过节点7找到节点6,通过节点6找到节点4,直到二进制位中仅剩一个1的时候结束。再对找到的所有节点求和就是我们所需要的总和。找点的过程中每次操作都消去末尾的一个1。(101),我们将它减去lowbit(101)就得到了他的下一个节点(100)。
以上是求节点n的前缀和的过程。当我们需要求区间和的时候,就需要用到前置知识点2中的方法。用大区间减去小区间就是中间区间的总和了。

区间和[6,7] = query(7) - query(5)

单点修改:自底向上修改Sum,时间复杂度logn。
区间查询:通过lowbit抹除末尾1并求和。最坏情况每一位都是1,抹除logn次,时间复杂度logn。

代码实现:

(如果使用0作为起始下标,lowbit操作就不再是找最低位的1,而是最低位的0。这里只介绍以1作为下标的解法)

int N,M;
const int MAXN = 1e6 + 10;
int arr[MAXN];//原始数组
int tr[MAXN];//树状数组的节点 存储区间和//lowbit 用来找孩子的父节点和求和时的下一个区间节点
int lowbit(int x){return x&-x;
}//add 将idx为下标的点加v,并通过加lowbit将它所有的祖先节点的Sum都加v 父节点最大不超过N
void add(int idx,int v){for(int i = idx; i <= N; i+= lowbit(i))tr[i] += v;
}//query 查询以r为结尾的总和 即[1,R]的区间和 通过减lowbit的操作找到下一个节点 直到找到0为止
int query(int r){int sum = 0;for(int i = r; i >= 1; i -= lowbit(i))sum += tr[i];return sum;
}int main(){cin >> N >> M;for(int i = 1;i <= N;i++)cin >> arr[i];//对树状数组初始化,相当于对所有元素加arr[i]for(int i = 1;i <= N;i++)add(i,arr[i]);while(M--){int k,a,b; cin>>k>>a>>b;if(k == 0){//利用前缀和求区间和cout << query(b) - query(a - 1) << endl;}else{//单点修改add(a,b);}}return 0;
}

总结:

  1. 线段树可以处理区间信息,例如最大值,最小值。不仅仅是和。
  2. 树状数组只能维护“操作和”,包括前缀和,前缀积等等。
  3. 二者的思想都是“空间换时间”,得到一个预处理数组,在预处理数组上进行查询和修改操作。
  4. 二者都是建立在数组上的树形结构。线段树拥有明确的二分结构,树状数组则没有。
  5. 线段树能做到的,树状数组未必能做到。树状数组能做到的,线段树也能做到。
  6. 从效率来说,如果只用于求和(积),更推荐写树状数组,代码精简,线段树的空间复杂度常数较大,占用空间也更多。且树状数组通常速度更快。

投稿来自 东北石油大学 - 软工194 - 徐文博

C++ 线段树,树状数组相关推荐

  1. 树套树 ----- P1975 [国家集训队]排队(树状数组套权值线段树求动态逆序对)

    解题思路: 首先我们知道交换两个数a[l]和a[r]a[l]和a[r]a[l]和a[r]影响到的区间是[l+1,r−1][l+1,r-1][l+1,r−1] 对于a[l]a[l]a[l],我们要减去[ ...

  2. 树套树 ---- 树状数组套权值线段树模板题 P2617 Dynamic Rankings 动态第K大

    题目链接 题目大意: 给你一个数组aaa,aaa有两个操作 询问aaa中[l,r][l,r][l,r]区间里面第kkk小的数是哪个? 修改axa_xax​为yyy 解题思路: 首先我们知道权值线段树是 ...

  3. poj 2352 Stars 线段树(先建后查/边建边查)/树状数组三种方法思路详解,带你深入了解线段树难度⭐⭐⭐★

    poj 2352 Stars 目录 poj 2352 Stars 1.树状数组 2.线段树,先建树后查找 3.线段树,边建树边查找 Description Astronomers often exam ...

  4. 8.8线段树和树状数组

    题目链接   http://acm.hust.edu.cn/vjudge/contest/view.action?cid=28619#overview 密码 acmore 还是感觉不怎么会线段树,还是 ...

  5. 线段树/树状数组问题 | 问题集合

    写在前面 线段树代码实在冗长,于是乎能用树状数组直接搞的就懒得打线段树了(:溜 1.P2620[QZYZ] 校门外的树 描述 Description 校门外有很多树,有苹果树,香蕉树,有会扔石头的,有 ...

  6. 花神游历各国 题解(小清新线段树/树状数组+并查集)

    题面 众所周知,这是一道小清新线段树 然而可以用树状数组水过去且跑得飞快 看到区间开方第一反应肯定是线段树懒标记区间修改之类的,但是这个东西似乎确凿不可维护 所以考虑暴力循环单点修改->T飞 于 ...

  7. LeetCode Range Sum Query - Mutable(树状数组、线段树)

    问题:给出一个整数数组,求出数组从索引i到j范围内元素的总和.update(i,val)将下标i的数值更新为val 思路:第一种方式是直接根据定义,计算总和时直接计算从i到j的和 第二种方式是使用树状 ...

  8. hdu 4417(线段树OR树状数组)

    题意:输入一个长度为n的序列,然后m个询问,询问区间[a,b]中比h小的数的个数. 思路:树状数组或线段树离线处理. 树状数组1 View Code 1 #include<cstdio> ...

  9. 【bzoj4881】[Lydsy2017年5月月赛]线段游戏 树状数组+STL-set

    题目描述 quailty和tangjz正在玩一个关于线段的游戏.在平面上有n条线段,编号依次为1到n.其中第i条线段的两端点坐标分别为(0,i)和(1,p_i),其中p_1,p_2,...,p_n构成 ...

  10. hdu2492 数状数组或者线段树

    题意:      给你一些人,每个人有自己的攻击力,输入的顺序就是每个人的顺序,他们之间互相比赛,两个人比赛的条件是必须在他们两个位置之间找到一个人当裁判,这个裁判的攻击力必须在他们两个人之间,问你最 ...

最新文章

  1. 使用rqt_console和roslaunch---ROS学习第7篇
  2. /usr/local/php-5.2.14/sbin/php-fpm start Starting php_fpm –fpm-config
  3. Android和ios速度,不拼硬件拼体验 Android和iOS系统的加载速度测验
  4. 根据定制的 XML 文件进行随机抽取节
  5. ubuntu的网络配置
  6. ASP.NET会员注册登录模块(MD5加密,Parameters防止SQL注入,判断是否注册)
  7. 是男人就下100层【第五层】——2048游戏从源码到发布市场
  8. 百度终于对知乎下手了:将以小程序接入百度App
  9. 构造函数后面的冒号后初始化列表
  10. php操作excel表格的导入和导出
  11. 怎么样成为一个高手--有悟
  12. HDU3032 Nim or not Nim?
  13. ISO IEC 27001 企业信息安全管理要求
  14. canvas绘制图形的相关API
  15. 部署Extmail邮件服务器教程——适用于小白
  16. 自动发货-用千牛如何做到发货号自动转接人工号
  17. R语言时间序列ARIMA新手教程
  18. NSSCTF web学习
  19. PHP对接支付宝支付APP端
  20. 计算机毕设Python+Vue学生社团管理系统(程序+LW+部署)

热门文章

  1. 计算机视觉SIFT算法详解
  2. 服务器和交换机物理连接_交换机发生网络通信故障怎么解决?
  3. Rank Scores(分数排序)
  4. 【BZOJ1814】Ural 1519 Formula 1 (插头dp)
  5. python中base函数_详细的python basemap中各函数的所有参量注释
  6. 洛达芯片检测工具AB153x_UT,检测蓝牙芯片协议
  7. 三极管工作原理及测定
  8. P4315 月下“毛景树” 树链剖分+线段树
  9. 【计算机网络】思科实验(3):使用三层交换机实现跨VLAN间的通信
  10. 一本书让你知道互联网思维 个人总结