树状数组学习

转载自:http://blog.csdn.net/int64ago/article/details/7429868

写下这个标题,其实心里还是没底的,与其说是写博帖,不如说是做总结。第一个接触树状数组还是两年前,用什么语言来形容当时的感觉呢?……太神奇了!真的,无法表达出那种感觉,她是那么的优雅,10行不到的代码,却把事情干的如此出色!没有了解她原理的前提下即使把代码倒背如流也理解不了!其中,我就是一直没搞懂地在使用她。时隔两年,又无意遇到了她,可能是两年的代码经验的积累,有了些新的认识,可以自信的说理解了吧!下面我争取用自己的方式让更多人明白她,而不是背诵她。为了更方便的说明,文章里会自己强加一些概念,只是为了更好的理解,不是什么专业术语之类的。

一、树状数组是干什么的?

平常我们会遇到一些对数组进行维护查询的操作,比较常见的如,修改某点的值、求某个区间的和,而这两种恰恰是树状数组的强项!当然,数据规模不大的时候,对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N),如果实时的对数组进行M次修改或求和,最坏的情况下复杂度是O(M*N),当规模增大后这是划不来的!而树状数组干同样的事复杂度却是O(M*lgN),别小看这个lg,很大的数一lg就很小了,这个学过数学的都知道吧,不需要我说了。申明一下,看下面的文章一定不要急,只需要看懂每一步最后自然就懂了。

二、树状数组怎么干的?

先看两幅图(网上找的,如果雷同,不要大惊小怪~),下面的说明都是基于这两幅图的,左边的叫A图吧,右边的叫B图:

是不是很像一颗树?对,这就是为什么叫树状数组了~先看A图,a数组就是我们要维护和查询的数组,但是其实我们整个过程中根本用不到a数组,你可以把它当作一个摆设!c数组才是我们全程关心和操纵的重心。先由图来看看c数组的规则,其中c8 = c4+c6+c7+a8,c6 = c5+a6……先不必纠结怎么做到的,我们只要知道c数组的大致规则即可,很容易知道c8表示a1~a8的和,但是c6却是表示a5~a6的和,为什么会产生这样的区别的呢?或者说发明她的人为什么这样区别对待呢?答案是,这样会使操作更简单!看到这相信有些人就有些感觉了,为什么复杂度被lg了呢?可以看到,c8可以看作a1~a8的左半边和+右半边和,而其中左半边和是确定的c4,右半边其实也是同样的规则把a5~a8一分为二……继续下去都是一分为二直到不能分,可以看看B图。怎么样?是不是有点二分的味道了?对,说白了树状数组就是巧妙的利用了二分,她并不神秘,关键是她的巧妙!

她又是怎样做到不断的一分为二呢?说这个之前我先说个叫lowbit的东西,lowbit(k)就是把k的二进制的高位1全部清空,只留下最低位的1,比如10的二进制是1010,则lowbit(k)=lowbit(1010)=0010(2进制),介于这个lowbit在下面会经常用到,这里给一个非常方便的实现方式,比较普遍的方法lowbit(k)=k&-k,这是位运算,我们知道一个数加一个负号是把这个数的二进制取反+1,如-10的二进制就是-1010=0101+1=0110,然后用1010&0110,答案就是0010了!明白了求解lowbit的方法就可以了,继续下面。介于下面讨论十进制已经没有意义(这个世界本来就是二进制的,人非要主观的构建一个十进制),下面所有的数没有特别说明都当作二进制。

上面那么多文字说lowbit,还没说它的用处呢,它就是为了联系a数组和c数组的!ck表示从ak开始往左连续求lowbit(k)个数的和,比如c[0110]=a[0110]+a[0101],就是从110开始计算了0010个数的和,因为lowbit(0110)=0010,可以看到其实只有低位的1起作用,因为很显然可以写出c[0010]=a[0010]+a[0001],这就为什么我们任何数都只关心它的lowbit,因为高位不起作用(基于我们的二分规则它必须如此!),除非除了高位其余位都是0,这时本身就是lowbit。

既然关系建立好了,看看如何实现a某一个位置数据跟改的,她不会直接改的(开始就说了,a根本不存在),她每次改其实都要维护c数组应有的性质,因为后面求和要用到。而维护也很简单,比如更改了a[0011],我们接着要修改c[0011],c[0100],c[1000],这是很容易从图上看出来的,但是你可能会问,他们之间有申明必然联系吗?每次求解总不能总要拿图来看吧?其实从0011——>0100——>1000的变化都是进行“去尾”操作,又是自己造的词--‘‘,我来解释下,就是把尾部应该去掉的1都去掉转而换到更高位的1,记住每次变换都要有一个高位的1产生,所以0100是不能变换到0101的,因为没有新的高位1产生,这个变换过程恰好是可以借助我们的lowbit进行的,k +=lowbit(k)。

好吧,现在更新的次序都有了,可能又会产生新的疑问了:为什么它非要是这种关系啊?这就要追究到之前我们说c8可以看作a1~a8的左半边和+右半边和……的内容了,为什么c[0011]会影响到c[0100]而不会影响到c[0101],这就是之前说的c[0100]的求解实际上是这样分段的区间 c[0001]~c[0001] 和区间c[0011]~c[0011]的和,数字太小,可能这样不太理解,在比如c[0100]会影响c[1000],为什么呢?因为c[1000]可以看作0001~0100的和加上0101~1000的和,但是0101位置的数变化并会直接作用于c[1000],因为它的尾部1不能一下在跳两级在产生两次高位1,是通过c[0110]间接影响的,但是,c[0100]却可以跳一级产生一次高位1。

可能上面说的你比较绕了,那么此时你只需注意:c的构成性质(其实是分组性质)决定了c[0011]只会直接影响c[0100],而c[0100]只会直接影响[1000],而下表之间的关系恰好是也必须是k +=lowbit(k)。此时我们就是写出跟新维护树的代码:

void add(int k,int num)
{
    while(k<=n)
    {
        tree[k]+=num;
        k+=k&-k;
    }
}

有了上面的基础,说求和就比较简单了。比如求0001~0110的和就直接c[0100]+c[0110],分析方法与上面的恰好逆过来,而且写法也是逆过来的,具体就不累述了:

int read(int k)//1~k的区间和
{
    int sum=0;
    while(k)
    {
        sum+=tree[k];
        k-=k&-k;
    }
    return sum;
}

三、总结一下吧

首先,明白树状数组所白了是按照二分对数组进行分组;维护和查询都是O(lgn)的复杂度,复杂度取决于最坏的情况,也是O(lgn);lowbit这里只是一个技巧,关键在于明白c数组的构成规律;分析的过程二进制一定要深入人心,当作心目中的十进制。

时间: 2024-10-04 02:01:34

树状数组学习的相关文章

树状数组学习资料1

1 一维树状数组 1 什么是树状数组 树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组A[1..n],那么查询A[1]+...+A[n]的时,间是log级别的,而且是一个在线的数据结构. 2 树状数组作用 我们经常会遇到动态连续和查询问题,给定n个元素A[1~N],让我们求sum[L,R] = A[L]+...+A[R],或者更改A[i]的值. 假设数据很小的时候,那么我们利用暴力就可以搞定,这个时候更改A[i]的复杂度为O(1),但是求和的复杂度为O(n),如果有m次求和就是

树状数组 学习笔记

树状数组可以用来求区间元素的和. 与前缀和做法不同,它支持值的修改. 比如说,现在我有一个数列a,要求你维护这个数列,使其支持两个操作. 1.改变数列第k项的值 2.查询从第i项到第j项的总值 暴力做法总是过不了所有点的,如果使用暴力,虽然操作1是O(1)的,但是操作2是O(n)的,没人对此复杂度满意. 假设原数列为a,我们的树状数组为c,那么,应该有下图的情况. 可以看出,每一个叶节点对应数组中的某个元素. c[i]为第i列树上最高的那个点. 数组c就是树状数组. 不难看出 对于每一个c[i]

树状数组学习(一维)

算法描述 可以对给定序列进行查询和修改 查询:主要用来查询任意两位之间数据和 修改:修改单项数据值 时间复杂度:log(n) 算法思想 1.数组的构建 定义 数组C A C1 = A1 C2 = A1 + A2 C3 = A3 C4 = A1 + A2 + A3 + A4 C5 = A5 C6 = A5 + A6 C7 = A7 C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 - 不难看出,其实是实现了对A的数据的分别管辖,Cn的管辖数量为 将n化为二进制数

hdu 5249区间第k大(学习了下树状数组的搞法)

KPI Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others) Total Submission(s): 205    Accepted Submission(s): 70 Problem Description 你工作以后, KPI 就是你的全部了. 我开发了一个服务,取得了很大的知名度.数十亿的请求被推到一个大管道后同时服务从管头拉取请求.让我们来定义每个请求都有一个重要值.我的K

【学习整理】树状数组 区间修改+查询

前言:对于区间修改和区间查询这样的简单问题,打一大堆线段树确实是不划算,所以学习了区间修改+区间修查询的树状数组. 我们定义 为原数列, ,显然  . 若想要将区间 的数全部 则只需要将 , 即可. 所以,我们维护一个数组 在将区间 的数全部 则还需同时将 , . 结论: 例题:CODEVS1082 线段树练习3 http://codevs.cn/problem/1082/ #include<iostream> #include<cstdio> #include<cstdli

学习笔记——二维树状数组

不知道为什么,就是想把这个坑给填了... 二维树状数组,本质上还是树状数组,只是在一维的基础上变成了二维... 单点修改  1到i,j查询和一维基本一样,直接上代码 #include<iostream> #include<cstdlib> #include<cstdio> #include<algorithm> #define N 3010 using namespace std; int a[N][N],n; inline int lowbit(int x

树状数组review学习

学习参考: https://blog.csdn.net/flushhip/article/details/79165701 https://blog.csdn.net/moep0/article/details/52770728 树状数组在寒假集训的时候其实有讲过,但是当时只是有了板子,没有很深地去学习理解,现在回来想去理解一下这个很好用的数据结构. 树状数组的作用 树状数组是对于一个用来处理动态更新.动态统计区间问题的一种良好的数据结构,查询和修改复杂度都为O(logn)的数据结构. 树状数组

树状数组 浅显学习

学习来源 首先要明确树状数组的本质就是带修改的前缀和,它每次用 lowbit 来很巧妙的寻找所属前缀的位置 在这些位置+k 然后还是用 lowbit 来查询这段和应该属于的树状数组的位置. 最简单的就是单点更新和区间查询,或者区间更新 int c[maxn];//树状数组 int n;//树状数组的大小 int lowbit(int x) { return x & (-x); } void update(int i,int k) { while(i>0) { c[i] += k; i -=

【算法学习笔记】40.树状数组 动态规划 SJTU OJ 1289 扑克牌分组

Description cxt的扑克牌越来越先进了,这回牌面的点数还可以是负数, 这回cxt准备给扑克牌分组,他打算将所有的牌分成若干个堆,每堆的牌面总和和都要大于零.由于扑克牌是按顺序排列的,所以一堆牌在原牌堆里面必须是连续的.请帮助cxt计算一下,存在多少种不同的分牌的方案.由于答案可能很大,只要输出答案除以1,000,000,009的余数即可. Input Format 第一行:单个整数:N,1 ≤ N ≤ 10^6 第二行到N + 1行:在第i + 1行有一个整数:Ai, 表示第i张牌牌