- 本人的LeetCode账号:魔术师的徒弟,欢迎关注获取每日一题题解,快来一起刷题呀~

  • 本人Gitee账号:路由器,欢迎关注获取博客内容源码。

  树状数组和其他的高级数据结构不同,它非常的好写,同时解决问题也比较局限,所以树状数组的题目的难度主要集中在思考而非代码。

一、基本原理

  树状数组可以解决两个操作:快速的求前缀和修改某一个数,这两个操作都是O(logn)的。

  这两个操作如果我们直接来操作:

  • 存原数组,前缀和O(N),修改一个数O(1)
  • 维护前缀和,前缀和O(1),修改一个数O(N)

  有一种鱼和熊掌不可兼得的感觉,但是我们的题目中时间复杂度一般取决于最糟糕的时间复杂度,所以如果有n次查询,那么复杂度会达到O(n^2)。树状数组有一个折中的思想,它让这两个操作的时间复杂度都变成了O(logn),这样总时间复杂度就是O(nlogn),就会快很多了。

  它是一种基于二进制的方法来解决这个问题的。

  假设我们有一个数x,其二进制表示为:
x=2ik+2ik−1+...+2i1ik>=ik−1>=...>=i1x = 2^{i_k} + 2^{i_{k - 1}} + ...+2^{i_{1}}\\ i_{k}>=i_{k-1}>=...>=i_1 x=2ik​+2ik−1​+...+2i1​ik​>=ik−1​>=...>=i1​
  假设我们想求的是下标为1~x的总和,那么我们可以把1~x这个区间划分成k部分:
(x−2i1,x](x−2i1−2i2,x−2i1]...(0,x−2i1−2i2−...−2ik−1](x-2^{i_1},x]\\(x-2^{i_1}-2^{i_2}, x-2^{i_1}]\\...\\(0,x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}] (x−2i1​,x](x−2i1​−2i2​,x−2i1​]...(0,x−2i1​−2i2​−...−2ik−1​]
  这样就把下标为1~x这个区间划分成了logx份,这样如果算1~x的总和,只需要求logx个区间的和就能算出来了。

  这个思想就是让我们在logn的时间复杂度中使用前缀和的思想。

  下面来看看区间中元素的个数和区间右端点有什么关系:
(x−2i1,x],元素个数21i个(x−2i1−2i2,x−2i1],元素个数22i个...(0,x−2i1−2i2−...−2ik−1],元素个数2ki个(x-2^{i_1},x], 元素个数2^i_1个\\ (x-2^{i_1}-2^{i_2}, x-2^{i_1}],元素个数2^i_2个\\ ...\\ (0,x-2^{i_1}-2^{i_2}-...-2^{i_{k-1}}],元素个数2^i_k个 (x−2i1​,x],元素个数21i​个(x−2i1​−2i2​,x−2i1​],元素个数22i​个...(0,x−2i1​−2i2​−...−2ik−1​],元素个数2ki​个
  区间中元素的个数就是右端点二进制的最低位1的所对应的2的幂。

  所以每个区间可以这样表示:[R - lowbit(R) + 1, R],其中lowbit(R) = R & (-R),因此我们可以用一个参数的函数来表示这个区间的总和:C[R]

  C[x]表示原数组的a[x - lowbit(x) + 1, x]的区间和,所以对原数组下标1~x的和,我们可以用logxC[x]的和就能求出来了。

  再考虑一下C[x]的关系。

  我们发现,父节点和子结点的关系:
c[x]=a[x]+c[x−1]+c[x−1−lowbit]+...+c[0](每次都从x−1去掉最后一个1)14:a[14]+c[13=(01101)2]+c[(01100)2](到0了不算了)13:a[13]+c[01100](到0了不算了)16:a[16]+c[15]+c[14]+c[12]+c[8]+c[0]c[x]=a[x] + c[x - 1] + c[x - 1 - lowbit] +...+c[0](每次都从x-1去掉最后一个1)\\ 14:a[14] + c[13=(01101)_2] + c[(01100)_2](到0了 不算了)\\ 13:a[13] + c[01100](到0了不算了)\\ 16:a[16] + c[15] + c[14] + c[12] + c[8] + c[0] c[x]=a[x]+c[x−1]+c[x−1−lowbit]+...+c[0](每次都从x−1去掉最后一个1)14:a[14]+c[13=(01101)2​]+c[(01100)2​](到0了不算了)13:a[13]+c[01100](到0了不算了)16:a[16]+c[15]+c[14]+c[12]+c[8]+c[0]
  如何通过子结点找父节点呢?(对应修改操作)

  修改一个值后,如何确定它会影响哪些父节点呢?

  其实就是上面操作的逆操作,对于一个x,找到其最后一段这样的东西01...10...0,让它进位成10...0即可,发现只要加上10...0(最低位的1的2的幂)即可,这就找到了它的直接父节点,即操作就是x + lowbit(x)

  修改操作add(int x, int c):对原数组aa[x4] += c,就是我们刚刚得到的它会影响哪些父节点,对每个数从0开始的初始修改其实就是创建树状数组。

for (int i = x; i <= n; i += lowbit(i)) tr[i] += c;

  查询操作sum(int x):求1~x的和,就是我们最早的C[i]的区间含义,tr[i]表示以下标i结尾的,长度为lowbit(i)的区间。

for (int i = x; i >= 0; i -= lowbit(i)) res += tr[i];

  树状数组常用于快速求得前缀和和修改元素组的值、统计一个数组左边或者右边比当前值大或小的元素个数。

  以Less[i] = [0,i)比nums[i]小的元素的个数的求解为例(假设nums元素全部为正),首先我们建立一个树状数组B,大小是nums的最大元素 + 1,然后从左往右遍历,每次先求一下B.sum(nums[i] - 1),它就是下标1~nums[i] - 1的和,而我们tr[i] = c表示i的元素出现了c次,所以其前缀和sum(x)就是小于等于x的元素出现的次数。

二、例题

1 楼兰图腾

  题意,给了平面上的n个点,它们的横坐标是1~n的一个有序排列,纵坐标是1~n的一个任意排列,统计一下有多少个三元组(i, j ,k),满足两边都比中间高i < j < k && yi > yj && yk > yj,这是第一问,第二问就是中间比两边高。

  数据范围是20w,意味着我们要用一个nlogn的算法。

  首先我们从一个集合考虑,假设一个集合中是所有的点,我们把它以横坐标为1 2 …n分为n个集合,这个划分是不重不漏的。

  那么我们就看看第k部分的满足条件的子集有多少个即可,我们只要统计出yk左边有多少点大于ykyk右边有多少点大于yk,利用乘法原理两个乘起来就行。

  我们可以从左到右扫描一遍,得到greater[k]表示1~k - 1有多少点的纵坐标大于yk,再从右往左遍历一遍,得到k + 1~n中有多少数大于yk

  统计一个区间的和,这是树状数组可以解决的问题。

  然后k统计完后,我们就给yk加1即可,这对应修改操作。

  上面写的很乱,建议思路看代码。

#include <iostream>
#include <cstring>
using namespace std;
typedef long long LL;
const int N = 200010;int n;
int a[N];// 表示点(i, a[i])
int Greater[N];// Greater[x]存当前比x大的数的个数
int Lower[N]; // Lower[x]存当前比x小的数的个数int tr[N]; // 树状数组对应的原数组含义就是数字i出现了a[i]次
// 因此其前缀和sum(i - 1)就表示小于i的数字出现了多少次
// 从左往右遍历时 它的sum表示当前点左边 1~x中的数字和
// 因为我们每次遍历完一个点就会add(y, 1)
// 所以其实sum(y - 1)表示当前点左边比y小的数字出现的次数
// 同理 从右往左遍历是 sum(y - 1)表示当前点右边出现过的比y小的数字int lowbit(int x)
{return x & (-x);
}void add(int k, int val)
{for (int i = k; i <= n; i += lowbit(i)) tr[i] += val;
}int sum(int x)
{int res = 0;for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];return res;
}int main()
{scanf("%d", &n);for (int i = 1; i <= n; ++i) scanf("%d", &a[i]);// 从左往右遍历 确定Greater和Lowerfor (int i = 1; i <= n; ++i){int y = a[i];// 当前区间左边比y小的数的出现次数Lower[i] = sum(y - 1);// 当前区间左边比y大的数的出现次数 最大的数是nGreater[i] = sum(n) - sum(y);// 这个点出现过了 给树状数组加上add(y, 1);}memset(tr, 0, sizeof(tr));// 清空树状数组LL res1 = 0;// V的个数LL res2 = 0;// ^的个数for (int i = n; i >= 1; i--){int y = a[i];// 统计当前点右边比y大的数的个数res1 += (LL)Greater[i] * (sum(n) - sum(y));// 统计当前点右边比y小的数字的个数res2 += (LL)Lower[i] * sum(y - 1);add(y, 1);}cout << res1 << ' ' << res2 << endl;return 0;
}

2 树状数组板子

class BIdxT
{public:BIdxT(int sz): tr(sz + 1){}int lowbit(int x) { return x & (-x); }void add(int k, int val){for (int i = k; i < tr.size(); i += lowbit(i)) tr[i] += val;}void reinit(){fill(tr.begin(), tr.end(), 0);}int sum(int x){int res = 0;for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];return res;}
private:vector<int> tr;
};

3 LeetCode307. 区域和检索 - 数组可修改

307. 区域和检索 - 数组可修改

  本题需要的两个操作是快速求得前缀和,快速修改一个数组中的值,显然可以用树状数组来处理。

class tr
{public:tr(const vector<int>& nums):t(nums.size() + 1){for (int i = 0; i < nums.size(); ++i) add(i + 1, nums[i]);}int lowbit(int x){return x & (-x);}void add(int x, int val){for (int i = x; i < t.size(); i += lowbit(i)) t[i] += val;}int sum(int x){int res = 0;for (int i = x; i > 0; i -= lowbit(i)) res += t[i];return res;}int query(int left, int right){return sum(right) - sum(left - 1);}
private:vector<int> t;
};class NumArray {public:NumArray(vector<int>& nums) : _tr(nums), num(nums){}void update(int index, int val) {_tr.add(index + 1, val - num[index]);num[index] = val;}int sumRange(int left, int right) {return _tr.query(left + 1, right + 1);}
private:tr _tr;vector<int>& num;
};

4 LeetCode327. 区间和的个数

327. 区间和的个数

  本题的关键在于意识到要找的满足数量的s(i, j)等价于
lower<=preSum[j]−presum[i]<=upperpreSum[j]−upper<=preSum[i]<=preSum[j]−lower,0<=i<j就是找[0,j)区间内满足preSum[i]属于上面那个范围的i的数量可以用树状数组维护,从左向右遍历时,得到小于preSum[j]−upper和小于preSum[j]−lower的数量作差即得到当前满足条件的数量,累计求和即可lower<=preSum[j] - presum[i] <= upper\\ preSum[j] - upper <= preSum[i] <= preSum[j] - lower,0<=i<j\\ 就是找[0,j)区间内满足preSum[i]属于上面那个范围的i的数量\\ 可以用树状数组维护,从左向右遍历时,得到小于preSum[j] - upper和小于preSum[j] - lower的数量\\ 作差即得到当前满足条件的数量,累计求和即可\\ lower<=preSum[j]−presum[i]<=upperpreSum[j]−upper<=preSum[i]<=preSum[j]−lower,0<=i<j就是找[0,j)区间内满足preSum[i]属于上面那个范围的i的数量可以用树状数组维护,从左向右遍历时,得到小于preSum[j]−upper和小于preSum[j]−lower的数量作差即得到当前满足条件的数量,累计求和即可

  本题的前缀和得到的数会比较大,用LL存一下,由于数据个数其实不多而且有负数,所以可以做一个离散化处理。

typedef long long LL;
// 树状数组
class BIndexT
{public:BIndexT(int sz): tr(sz + 1){}int lowbit(int x){return x & (-x);}void add(int k, int val){for (int i = k; i < tr.size(); i += lowbit(i)) tr[i] += val;}int sum(int x){int res = 0;for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];return res;}
private:vector<int> tr;
};
class Solution {public:int countRangeSum(vector<int>& nums, int lower, int upper) {vector<LL> preSum(nums.size() + 1);// 求解前缀和for (int i = 0; i < nums.size(); ++i){preSum[i + 1] = preSum[i] + nums[i];}vector<LL> alls;for (LL p : preSum){alls.push_back(p);alls.push_back(p - upper);alls.push_back(p - lower);}// 离散化sort(alls.begin(), alls.end());alls.erase(unique(alls.begin(), alls.end()), alls.end());unordered_map<LL, int> fix;int idx = 0;for (LL num : alls){fix[num] = idx++;}// 创建树状数组 其sum表示小于x的元素出现的次数BIndexT B(alls.size());int res = 0;// 遍历前缀和数组// 查询前缀和下标(0, i)中值处于区间[preSum[i] - upper, preSum[i] - lower]的个数for (int i = 0; i < preSum.size(); ++i){LL p = preSum[i];int l = fix[p - upper];int r = fix[p - lower];int L = B.sum(l);int R = B.sum(r + 1);res += R - L;B.add(fix[p] + 1, 1);}return res;}
};

5 LeetCode.1395统计作战单位数

1395. 统计作战单位数

  本题也是经典的树状数组应用题:统计左边有多少元素比自己小Less[i],统计左边有多少元素比自己大Greater[i],统计右边有多少元素比自己小,统计右边有多少元素比自己大。

  因为士兵得分都是正值,所以不必离散化直接干就完了。

class BIdxT
{public:BIdxT(int sz): tr(sz + 1){}int lowbit(int x) { return x & (-x); }void add(int k, int val){for (int i = k; i < tr.size(); i += lowbit(i)) tr[i] += val;}void reinit(){fill(tr.begin(), tr.end(), 0);}int sum(int x){int res = 0;for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];return res;}
private:vector<int> tr;
};
class Solution {public:int numTeams(vector<int>& rating) {vector<int> Greater(rating.size());vector<int> Lower(rating.size());int n = *max_element(rating.begin(), rating.end());BIdxT B(n);for (int i = 0; i < rating.size(); ++i){int score = rating[i];Greater[i] = B.sum(n) - B.sum(score);Lower[i] = B.sum(score - 1);B.add(score, 1);}B.reinit();int res1 = 0;// > >int res2 = 0;// < <for (int i = rating.size() - 1; i >= 0; --i){int score = rating[i];res1 += Greater[i] * B.sum(score - 1);res2 += Lower[i] * (B.sum(n) - B.sum(score));B.add(score, 1);}return res1 + res2;}
};

6 LeetCode 315. 计算右侧小于当前元素的个数

315. 计算右侧小于当前元素的个数

  本题也是比较典型的寻找该位置右边比自己小的元素的个数,由于有负数,所以要离散化处理一下。

class BIdxT
{public:BIdxT(int sz): tr(sz + 1){}int lowbit(int x) { return x & (-x); }void add(int k, int val){for (int i = k; i < tr.size(); i += lowbit(i)) tr[i] += val;}void reinit(){fill(tr.begin(), tr.end(), 0);}int sum(int x){int res = 0;for (int i = x; i > 0; i -= lowbit(i)) res += tr[i];return res;}
private:vector<int> tr;
};
class Solution {public:vector<int> countSmaller(vector<int>& nums) {// 离散化vector<int> alls(nums);sort(alls.begin(), alls.end());alls.erase(unique(alls.begin(), alls.end()), alls.end());unordered_map<int, int> myhash;int idx = 1;for (int p : alls){myhash[p] = idx++;}BIdxT B(alls.size());vector<int> Lower(nums.size());for (int i = nums.size() - 1; i >= 0; --i){int num = nums[i];Lower[i] = B.sum(myhash[num] - 1);B.add(myhash[num], 1);}return Lower;}
};

高级数据结构1—初识树状数组—快速求得前缀和和修改某一元素值相关推荐

  1. P4062 [Code+#1]Yazid 的新生舞会(区间绝对众数+分治/树状数组维护高维前缀和)

    P4062 [Code+#1]Yazid 的新生舞会 杭电多校懂得都懂 Code1 分治 比较喜欢分治的做法,非常好写.skylee大佬题解 首先对于任何一个区间来说,由于两个端点不确定性非常难以一次 ...

  2. 数据结构一【树状数组】普通、二维、离线树状数组的(单点修改,单点查询,区间修改,区间查询)模板及应用例题总结

    文章目录 树状数组 lowbit 线段树与树状数组 单点修改 区间查询 区间修改 区间求和 二维树状数组 离线树状数组 例题 POJ:stars MooFest [SDOI2009]HH的项链 Tur ...

  3. 树状数组——快速定位中位数

    序: 对于一个乱序的数组,我们搜寻它的中位数的常规方法就是排序.然而,在时间限制的情况下,排序显然不能满足我们的要求,因此我们这里引进树状数组. 几个套路 1.lowbit函数 用来搜寻一个数x的最低 ...

  4. 树状数组求逆序对_初识树状数组

    树状数组是用来解决数列多次单点修改和前缀和查询的利器. 首先我们来看问题的原型: 已知一个长度为n(n<=10 0000)的数列,初始值都是零,现在我们要对数列施加两种类型的操作共q(q< ...

  5. 树状数组 ---- 树状数组+动态维护前缀中位数 D. Omkar and Medians

    题目大意: 解题思路: 首先我们看他们的定义: bib_ibi​是a1,a2,a3,....,ai,ai+1,.......a2i−1a_1,a_2,a_3,....,a_i,a_{i+1},.... ...

  6. 牛客竞赛数据结构专题班树状数组、线段树练习题

    F.little w and Discretization 题意:找区间[l,r]内离散化后和原来的值不同大小的数的个数 思路:先求区间mex,同时记录区间有多少个数,再 用区间长度减去(区间内小于m ...

  7. [树状数组] Galahad

    题意:求给定区间内不同数的和 经典例题https://vjudge.net/problem/HDU-3333 解题思路: 这两天有点傻,emmm 离线操作 扫一遍数组 对于重复的值树状数组维护最靠近当 ...

  8. 树状数组(求子区间和+更新元素值)

    树状数组 欲完成修改值和查询区间和两种操作 求前缀和的做法时间复杂度为O(n)O(n)O(n) 使用树状数组时间复杂度降为O(logn)O(logn)O(logn) lowbit 1.x&(- ...

  9. js 数组 实现 完全树_算法和数据结构 | 树状数组(Binary Indexed Tree)

    本文来源于力扣圈子,作者:胡小旭.点击查看原文 力扣​leetcode-cn.com 树状数组或二叉索引树(英语:Binary Indexed Tree),又以其发明者命名为 Fenwick 树.其初 ...

最新文章

  1. oracle自动备份
  2. 中国肠道大会 | 日程及嘉宾(4月16日更新)
  3. 【转】Silverlight 3 Beta 新特性解析(7)- Child Window和Shader Effect篇
  4. 哈工大c语言编程题中国大学mooc第四周,中国大学MOOC哈工大C语言程序设计精髓第六周编程题答案.doc...
  5. Oracle安装时忘记设置密码
  6. sublime配置python开发环境_win7 下搭建sublime的python开发环境的配置方法
  7. 每个程序员都应该挑战的6个项目
  8. java win10 32,Win10 同时安装64位和32位的JDK
  9. xshell使用xftp传输文件和使用pure-ftpd搭建ftp服务
  10. c语言二叉树图形输出,C语言数据结构树状输出二叉树,谁能给详细的解释一下...
  11. 总有一些人在祖国需要的时候挺身而出
  12. qt中 accept()和ignore()函数
  13. JavaScript 时间戳(互相转换)(自定义格式)- 案例篇
  14. 【IScroll深入学习】解决IScroll疑难杂症
  15. 静心的全部秘密:你是观照者
  16. OpenJDK构建工具IcedTea 1.7发布
  17. 常见前端面试题之盒子模型
  18. 正态分布(近似正态分布)
  19. conda,anaconda,miniconda的区别
  20. python告诉你迪丽热巴 vs 杨幂 vs 林志玲谁最美

热门文章

  1. 2020年数学建模国赛A题题目和解题思路
  2. 基尔霍夫(kirchhoff)矩阵树定理
  3. 初学电子快速入门的方法
  4. 关于onCreate(Bundle savedInstanceState, PersistableBundle persistentState)
  5. DOTA2利雅得大师赛利用api多线程对选手数据和战队数据爬取与分析
  6. Linux-数据库自动备份
  7. Java_取模/取余
  8. linux登陆mysql数据库
  9. 夯实基础之C语言基础算法
  10. nba2k 服务器支持,NBA2K Online篮球在线官方网站-拼出你的传奇-腾讯游戏