• 树状数组(超详细)


          树状数组(binary indexed tree,发明者Peter M.Fenwick 1994),是一种设计新鲜的数组结构,它能够高效地获取数组中连续 k 个数的和

            概括说,树状数组通常用于解决以下问题:

            数组A中的元素可能不断地被修改,怎样才能快速地获取连续几个数地和?

    介绍

    什么是树状数组?

    我们给定一个数组A[ ],我们设一个数组C[ ]满足

    C[1] = A[1]

    C[2]=A[1]+A[2]" role="presentation" style="position: relative;">C[2]=A[1]+A[2]

    C[3] = A[3]

    C[4]=A[1]+A[2]+A[3]+A[4]" role="presentation" style="position: relative;">C[4]=A[1]+A[2]+A[3]+A[4]

    C[5] = A[5]

    C[6]=A[5]+A[6]" role="presentation" style="position: relative;">C[6]=A[5]+A[6]

    C[7] = A[7]

    C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]" role="presentation" style="position: relative;">C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8]

    ……

    那么C数组就是树状数组。

    思考:

    C[i] = ?

    实际上,我们设 k" role="presentation" style="position: relative;">k 为(下标)对应二进制末位0的个数i 从 1" role="presentation" style="position: relative;">1 开始算!

    那么:C[i] = A[i - 2 ^ k + 1] + ... + A[i]

    那么,问题来了给定i,如何求2k" role="presentation" style="position: relative;">2k

    答案很简单:2 ^ k = i & (i)" role="presentation" style="position: relative;">(i)

    关于i & (i)" role="presentation" style="position: relative;">(i)

    比如对于“010101000”最末有3个0,k" role="presentation" style="position: relative;">k 的值应该是3,算出的2k" role="presentation" style="position: relative;">2k应该为8

    110101000    ->     这是-i的原码

    101010111     ->     这是-i的反码

    101011000     ->     这是-i的补码

    010101000     ->     这是i的补码

    000001000     ->     这是(i & (-i))

    (000001000)_{2} = (8)_{10}

    我们定义它为lowbit(x)=x" role="presentation" style="position: relative;">lowbit(x)=x&(-x)

    c++简短函数代码:

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

    求和:

    当我们求A[1]+...+A[x]" role="presentation" style="position: relative;">A[1]+...+A[x]的之和时,

    C[x]如果包含的不一定是1...x" role="presentation" style="position: relative;">1...x的全部和,(比如C[6]=A[5]+A[6]" role="presentation" style="position: relative;">C[6]=A[5]+A[6])就需要再找一个C[k](显然k<x" role="presentation" style="position: relative;">k<x)累加起来,这个k" role="presentation" style="position: relative;">k我们称之为x前驱,举个例子:

    A[1]+A[2]+...+A[6]=C[6]+C[4]" role="presentation" style="position: relative;">A[1]+A[2]+...+A[6]=C[6]+C[4]

    A[1] + A[2] + .. + A[7] = C[7] + C[6] + C[4]

    前驱的编号即为比自己小的,最近的,最末连续0比自己多的数

    所以x的前驱k=xlowbit(x)" role="presentation" style="position: relative;">k=xlowbit(x),相当于剪掉了自己最左边的1

    求和函数:GetSum(x),代码如下:

    1. int getSum(int x){
    2. int ans = 0;
    3. for(int i = x; i > 0; i -= lowbit(i)) ans += C[i];
    4. return ans;
    5. }

    直接用一个循环求得Sum,时间复杂度为O(logn)" role="presentation" style="position: relative;">O(logn)

    求区间[x,y]之和怎么办?

    getsum(y)getsum(x1)" role="presentation" style="position: relative;">getsum(y)getsum(x1)

    修改:

    修改了某个A[i] ,就需要改动所有包含A[j]" role="presentation" style="position: relative;">A[j]C[i]

    从图上看就是要更改从叶子节点到根节点路径上所有的C[i]

    怎么求一个节点的父节点?

    经过观察和探究,前人们得出了这个规律:
    父亲:比自己大的,最近的,末位连续0比自己多的数

    x节点父亲的编号=x+lowbit(x)" role="presentation" style="position: relative;">=x+lowbit(x)

    修改函数modify()

    1. void modify(int x, int d){
    2. for(int i = x; i <= n; i += lowbit(i)) C[i] += d;
    3. }

    其时间复杂度依旧为O(logn)" role="presentation" style="position: relative;">O(logn)

    一个小小的问题:

    那原本的数组C[i],应该怎么做:

    其实调用修改函数,相当于把没有修改为一个值,代码如下。

    1. cin >> n;
    2. for(int i = 1; i <= n; i++){
    3. cin >> x;
    4. modify(i, x);
    5. }

    小结:

    1、在很多情况下,线段树都可以用树状数组实现,凡是能用树状数组实现的一定能用线段树。

    2、当题目不满组减法原则的时候,就只能用线段树,不能用树状数组。

    3、树状数组的时间复杂的每一个操作都是O(logn)" role="presentation" style="position: relative;">O(logn)的时间复杂度。

    这篇文章就在一个代码中结束了!

    觉得博主写的不错的关注支持一下吧!我会继续努力的~

     

  • 相关阅读:
    你应该知道的Linux内核基础及内核编译
    FFmpeg合并音视频文件操作备忘(mac版)
    顶级理解,阿里这份Github星标63.7K的Redis高级笔记简直不要太细
    jenkins
    leetcode 42, 58, 14(*)
    基于JAVA气候分析平台计算机毕业设计源码+数据库+lw文档+系统+部署
    笔试强训48天——day14
    【QT HTTP】使用QtNetwork模块制作基于HTTP请求的C/S架构
    DeFi 前景展望:概览主流 DeFi 协议 Q2 进展
    数字IC笔试面试题之--时钟偏斜(skew)与抖动(jitter)
  • 原文地址:https://blog.csdn.net/Jiguoyuanyic/article/details/126502780