树状数组

  • 从一个简单的问题谈起

    考虑一个简单的问题,给定一串有序数字A1...An并不断进行插入数的操作且保证始终有序,在修改过程中陆续给定若干数字N1,N2...Nm,求解A1到ANi的数的和的大小。

    我们试想,如果维护一个简单的array A来存储这串数的话,插入不是问题,时间是O(1),但是问题是如果询问m次,有n个数的话,查询区间大小的时间是O(n * m),显然如果在n和m较大的情况下是十分耗时的。那么换种思路,如果我们维护一个array B存储的是从1到i这段区间的数的和。那么会使查询时间优化至O(1),但面临的问题是每次修改都会需要变化从修改位置pos到n的所有数字的数值大小,显然修改操作是O(n * m)的时间复杂度。也是十分耗时的。
    那么有没有一种数据结构可以解决这种问题呢。或者说,使插入和查找时间复杂度都折中的一种结构?那么下面介绍的就是插入和查找都是O(mlogn),那么他怎么实现的呢?

  • 写在树状数组之前

    再次考虑一个问题,给定一个数N,如何将其操作后变成二进制只保留其最低位的1呢。举个例子,如10110(十进制24),经过操作以后变成00010。

    这个问题的其实还是很简单的,无非是找到一个方法使得保留最低位的1的情况下,使得别的位取反或置0。那么可以想到一个方法就是将原数取反后加1,这样在最低位的1高位的数仍然是与原数对应位置的数是相反的,而最低位1低位的数则全部由1进位为0,取反后成为0的最低位1经过进位后重新成为1.得到的新的数与原数进行与操作,即可得到题意求解的答案。
    拿举的例子操作一下,00010110 取反后 -> 11101001 加1-> 11101010 与原式子00010110与 -> 00000010,得解
    如果写成代码如下:

    x & -x

    这就是树状数组的灵魂操作——lowbit,它的做什么的呢,下面做介绍。

  • 先浅谈下状数组的表象
    特别强调了,虽然它的名字后缀是数组,但是归根到底,这就是棵树。先看看他的样子:树状数组
    其中array A代表的是原串数组,而C便是树状数组。我们先通过直观感受找找规律。
    C[1] = A[1]
    C[2] = C[1] + A[2] = A[1] + A[2]
    C[3] = A[3]
    C[4] = C[2] + C[3] + A[4] = A[1] + A[2] + A[3] + A[4]
    C[5] = A[5]
    C[6] = C[5] + A[6] = A[5] + A[6]
    C[7] = A[7]
    C[8] = C[4] + C[6] + C[7] + A[8] = (C[2] + C[3] + A[4]) + (A[6] + C[5]) + A[7] + A[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8]
    通过上面的八个式子,我们可以感受到树状数组mlogn的原理——二分
    C[2]是分为1 (A[1])和 2 [C[1]]两边,C[4]是分为1,2 (C[2])一边,3,4 (C[3]A[4])一边。C[8]是分为1~4 (C[4])一边,5~8 (C[6],C[7],A[8])一边。经过二分,在查找的时候,时间复杂度就降到了O(n),举个例子,如果我们要计算1~5这段区间的值的话,就是C[4] + C[5],同样,如果要是计算1~7的值,其实就是C[4] + C[6] + A[7],说到这里可能很多不知道为什么要这么计算,只是从表面上看是这样算出来的,其实它的计算过程是这样的,拿到一个数n我们先看它满足的最高位1是多少,如5和7都是100也就是4,那么加上C[4],然后进一步求次高位的1,发现5不满足110而7满足,所以7再加上C[6],发现5满足101所以加上C[5],而7已经包含110了,所以继续往下找是111,也满足,再加上C[7]。得解。
    当然,或许有人说,当添加7的时候,对C[7]还是未知的,那么其实也很简单,只需要利用递归的思想,求解1~6的和后再加上A[7].
  • array C与array A的关系
    现在的当务之急或许应该是求得怎么通过array A求array C,这就用上了前面提到的lowbit操作。我们还是通过观察找规律先,下面我们统一用二进制描述数字,这样比较方便。
    C[0001] = A[0001] (0001对应的是从0001开始递减1的1个数)
    C[0010] = A[0001] + A[0010] (0010对应的是从0010开始递减1的2个数)
    C[0011] = A[0011] (0011对应的是从0011开始递减1的1个数)
    C[0100] = A[0001] + A[0010] + A[0011] + A[0100] (0100对应的是从0100开始递减1的4个数)
    C[0101] = A[0101] (0101对应的是从0101开始递减1的1个数)
    C[0110] = A[0101] + A[0110] (0110对应的是从0110开始递减1的2个数)
    C[0111] = A[0111] (0111对应的是从0111开始递减1的1个数)
    C[1000] = A[0001] + A[0010] + A[0011] + A[0100] + A[0101] + A[0110] + A[0111] + A[1000] (1000对应的是从1000开始递减1的8个数)
    通过上面的式子,我们是不是可以清晰的发现,其实C[i]就是从i开始的递减1的lowbit(i)个数?这样我们就得到了array A和array C的关系,下面就剩两个解决问题的操作,即如何进行求和和插入修改?
  • 两个关键操作
    对于添加操作,我们发现,从二进制考虑其实是改变包含与它的所有高位1,在8这个范围里,我们举个例子,这样可以参考上面的图。如果添加3的话,需要改变哪些数字?显然可以得到需要改变C[4],C[8],为什么不需要改变剩下的数字呢,因为它们完全不包含3这个数字。由上面我们得到的结论,每个数字i只包含从该数开始的递减1的lowbit(i)个数,所以反之添加的时候我们需要改变的其实就是从该数字i开始的x个lowbit(i)个数,且这是一种包含操作,即每次需要更新i的值。
    按照3我们进行计算,0011,lowbit(0011) = 0001, 0011+0001 = 0100,此时改变C[4]的值。i = 0100,lowbit(0100) = 0100,0100 + 0100 = 1000,改变C[8]的值,下次操作肯定超出数值范围8,操作结束。
    如果用代码表示的话,则如下:

    void add(elemtype x, val){
        tree[x] += val;
        x += x & -x;
    }

    解决了插入操作后,求和操作就非常简单了,其实就是添加的反操作,就是将从数字i开始递减lowbit(i)的数对应的树状数组的值进行叠加。当然也是包含操作,代码如下:

    elemtype check(elemtype x){
        elemtype sum = 0;
        while(x > 0){
            sum += tree[x];
            x -= x & -x;
        }
    }
  • 树状数组小结
       树状数组可以解决很多问题,最经典的莫过于多有真子集问题,当模型和模型之间存在着包含关系的时候,求解每个集合真子集的个数等问题可以用树状数组轻松解决。我的另外几篇文章就是介绍了有关几道树状数组的题目,有兴趣可以加以研究。树状数组能让在维护数组的基础上求和的时间复杂度在O(mlogn)级别,在面对大量的n的时候尤为有效。而且它的思想很简单,代码十分简洁,是为广大算法爱好者所喜欢和欣赏的一个数据结构。

查看原文:http://chilumanxi.org/2016/02/05/%e6%a0%91%e7%8a%b6%e6%95%b0%e7%bb%84/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Go语言(也称为Golang)是由Google开发的一种静态强类型、编译型的编程语言。它旨在成为一门简单、高效、安全和并发的编程语言,特别适用于构建高性能的服务器和分布式系统。以下是Go语言的一些主要特点和优势: 简洁性:Go语言的语法简单直观,易于学习和使用。它避免了复杂的语法特性,如继承、重载等,转而采用组合和接口来实现代码的复用和扩展。 高性能:Go语言具有出色的性能,可以媲美C和C++。它使用静态类型系统和编译型语言的优势,能够生成高效的机器码。 并发性:Go语言内置了对并发的支持,通过轻量级的goroutine和channel机制,可以轻松实现并发编程。这使得Go语言在构建高性能的服务器和分布式系统时具有天然的优势。 安全性:Go语言具有强大的类型系统和内存管理机制,能够减少运行时错误和内存泄漏等问题。它还支持编译时检查,可以在编译阶段就发现潜在的问题。 标准库:Go语言的标准库非常丰富,包含了大量的实用功能和工具,如网络编程、文件操作、加密解密等。这使得开发者可以更加专注于业务逻辑的实现,而无需花费太多时间在底层功能的实现上。 跨平台:Go语言支持多种操作系统和平台,包括Windows、Linux、macOS等。它使用统一的构建系统(如Go Modules),可以轻松地跨平台编译和运行代码。 开源和社区支持:Go语言是开源的,具有庞大的社区支持和丰富的资源。开发者可以通过社区获取帮助、分享经验和学习资料。 总之,Go语言是一种简单、高效、安全、并发的编程语言,特别适用于构建高性能的服务器和分布式系统。如果你正在寻找一种易于学习和使用的编程语言,并且需要处理大量的并发请求和数据,那么Go语言可能是一个不错的选择。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值