• 算法结构-树状数组


    为什么要用树状数组?

            一般地,对于一个给定区间  进行区间更新和查询,都需要线性遍历该区间,时间复杂度为O(n) ,当查询次数较多时,可能会超时。而树状数组是一类通过树型结构存储,将区间处理复杂度降为 lgn的数据结构。

    树状数组的定义

            树状数组的思想是,将一些小节点的信息同时储存在对应的大节点中,大节点的信息同时存储在更大的节点中,以此类推。这样,我们对某个区间进行更新或查询时,就不需要遍历所有的基础节点,而只需要遍历保存了这些基础节点信息的大节点即可。这些存储区间信息的树型结构就被称为树状数组

    图片示例(自己画的不是很好看,哈哈)

    最下面的八个黄色方块就代表数组m,记录着要处理数组的原始数据。而上方参差不齐的白色方块,则代表着记录数组 m信息的树状数组n。图中的箭头代表信息存储的位置,如图可以看出,n1管理的就是m1,n2管理的是m1、m2,而n8管理的则是全部m1-m8个数。

    lowbit 函数

    那么,我们怎么知道 n管理的是 m中的哪个区间,又怎么知道m的信息都存放在哪些n节点中呢?我们引入一个函数:lowbit 来完成这件事情。

    1. int lowbit(int x) {
    2. return x & -x;
    3. }

    在常见的计算机中,有符号数采用补码表示。在补码表示下,数 x的相反数-x的补码表示为 x补+1。所以,lowbit 函数可以保留x二进制表示中最低位的1及其后面的全部0。 例如,6的二进制表示为110,其 lowbit值为10。 15的二进制表示为1111,其 lowbit值即为1。而对于2的幂次如8 = 1000,其lowbit值即为自己。

    lowbit 值有什么作用呢?我们以6为例,借助上图来看:6加上其 lowbit值2后结果为8。8节点刚好是存储了节点 6信息的,且比 6更大一级的节点;同样地, 6减去其 lowbit 值2后结果为4。 4节点存储的是 m1-m4的信息,刚好是与 6节点存储区间( 节点存储的是 m5-m6的信息)没有交集的节点。这两个规律,对于所有的值都通用。

    所以,有了 lowbit 函数,我们便可以开始了解树状数组的一些基本操作。为了方便,我们下面的代码针对求和一项属性来介绍:

    单点更新与区间查询

    在树状数组中, mi的信息首先一定是存储在ni中的,在这之后,我们每次将c节点的下标 i 加上lowbit(i)便可以得到更上一级的,也应存储当前信息的 m节点。所以,树状数组中单点更新的代码模板如下:

    1. void update(int pos, int k) { // m[pos] += k
    2. // n 为树状数组长度
    3. while(pos <= N) {
    4. n[pos] += k;
    5. // 找更上一级的 c 节点下标
    6. pos += lowbit(pos);
    7. }
    8. }

    当我们要查询数组  的某前缀信息时,例如求  的值,便从  开始查询,若  刚好记录的是  的和,那么直接返回即可;否则,要每次将  节点的下标  减去 ,以找到更前面的信息存储的  节点。所以,树状数组中区间查询的代码模板如下:

    1. long long askis(int pos) { // 求 sum(a[1] - a[pos])
    2. if(!pos) return 0;
    3. ll ret = 0;
    4. while(pos) {
    5. ret += n[pos];
    6. // 找更靠前的 c 节点下标
    7. pos -= lowbit(pos);
    8. }
    9. return ret;
    10. }

    根据以上两个基本操作,我们还可以拓展一些操作:

    1. long long asksi(int l, int r) { // 查询 a 数组 [l, r] 区间和
    2. return askis(r) - askis(l - 1);
    3. }
    4. long long askss(int pos) { // 查询 a[pos] 单点的值
    5. return askis(pos) - askis(pos - 1);
    6. }

    区间更新与区间查询:

    那么,我们如何进行区间更新呢?

    根据前缀和与差分数组的知识,如果我们想对某个区间进行更新,我们只需对其差分数组的区间两端进行更新,并在查询时进行前缀求和即可。而在之前的介绍中,我们发现,树状数组可以以  的复杂度查询某个数组  的前缀和。所以,我们可以用树状数组来解决区间更新和查询的问题。

    假设我们要对数组  进行区间更新及查询,那么我们维护序列  的差分数组 。也即,有 。区间更新时,便只需要对差分数组  进行单点更新;区间查询时,假设我们对  的一个前缀  求和,结果即为:

    所以,原数组  的区间查询问题,便可以通过维护两个树状数组,分别记录  和  的值即可。

    在代码中,第一个树状数组  就是 sum,第二个树状数组  则用 ntimessum 表示,对应的区间更新代码如下:

    1. void update(int pos, int k) {
    2. int x = pos;
    3. while(pos <= n) {
    4. sum[pos] += k;
    5. ntimessum[pos] += k * (x - 1);
    6. pos += lowbit(pos);
    7. }
    8. }
    9. void update_internal(int l, int r, int k) { // a[l]-a[r] 加上指定值 k
    10. // 类似于前缀和 + 差分
    11. update(l, k);
    12. update(r + 1, -k);
    13. }

    而当我们要对区间  进行区间查询时,代码如下

    1. long long askii(int pos) {
    2. if(!pos) return 0;
    3. ll ret = 0, x = pos;
    4. while(pos) {
    5. ret += x * sum[pos] - ntimessum[pos];
    6. pos -= lowbit(pos);
    7. }
    8. return ret;
    9. }
    10. long long asklr(int l, int r) { // 查询 [l, r] 区间和
    11. return askii(r) - askii(l - 1);
    12. }

    综上所述,我们介绍了如何利用树状数组进行区间处理问题。下面,我们给出整体模板。

    1. class BIT {
    2. using ll = long long;
    3. const int N = 1e5 + 5;
    4. public:
    5. int n;
    6. vector<ll> sum;
    7. vector<ll> ntimessum;
    8. BIT(): n(N), sum(N + 5, 0), ntimessum(N + 5, 0) {}
    9. BIT(int _n): n(_n + 5), sum(_n + 10, 0), ntimessum(_n + 10, 0) {}
    10. ll lowbit(ll x) {
    11. return x & (-x);
    12. }
    13. void update(int pos, ll k) { // 单点更新
    14. ll x = pos;
    15. while(pos <= n) {
    16. sum[pos] += k;
    17. ntimessum[pos] += k * (x - 1);
    18. pos += lowbit(pos);
    19. }
    20. }
    21. void update_internal(int l, int r, int k) { // 区间更新
    22. update(l, k);
    23. update(r + 1, -k);
    24. }
    25. ll askis(int pos) { // 区间更新后的单点查询、单点更新后的范围查询
    26. if(!pos) return 0;
    27. ll ret = 0;
    28. while(pos) {
    29. ret += sum[pos];
    30. pos -= lowbit(pos);
    31. }
    32. return ret;
    33. }
    34. ll asksi(int l, int r) { // 单点更新后的区间查询
    35. if(l > r) {
    36. //cout << "Interval invalid!\n";
    37. return 0;
    38. }
    39. return askis(r) - askis(l - 1);
    40. }
    41. ll askss(int pos) { // 单点更新后的单点查询
    42. return askis(pos) - askis(pos - 1);
    43. }
    44. ll askii(int pos) { // 区间更新后的前缀查询
    45. if(!pos) return 0;
    46. ll ret = 0, x = pos;
    47. while(pos) {
    48. ret += x * sum[pos] - ntimessum[pos];
    49. pos -= lowbit(pos);
    50. }
    51. return ret;
    52. }
    53. ll asklr(int l, int r) { // 区间更新后的区间查询
    54. return askii(r) - askii(l - 1);
    55. }
    56. };

    以下是针对模板的一些说明:

    首先,与一般数组不同,我们的树状数组下标是从  开始的。所以,如果需要进行下标访问,需要将题目中的下标 。

    其次,如果需要进行单点更新 + 区间查询操作,应使用 update 函数以及 asksi 函数;如果需要进行区间更新 + 区间查询操作,则需要使用 update_internal 及 asklr 函数。

    最后,本模板只针对求和功能。针对一些其他类型题目,比如较为经典的最长上升子序列问题,也可以修改本模板来使用。

  • 相关阅读:
    申请免费通配符证书
    redis(其它操作、管道)、django中使用redis(通用方案、 第三方模块)、django缓存、celery介绍(celery的快速使用)
    易基因|ONT:三代原核甲基化在痤疮杆菌噬菌体表观遗传印迹中的工程选择性研究
    设计模式之装饰器模式
    在网易有数上做数据加工和数据分析的实践
    美团一面面经及详细答案
    【21天学习经典算法】冒泡排序(附Python完整代码)
    HTML+CSS个人静态网页设计
    零基础学Python有什么建议?如何入门编程?
    aliyunoss上传图片
  • 原文地址:https://blog.csdn.net/qq_41306849/article/details/125506007