史上最易懂的树状数组入门教程

合集下载

树状数组及其应用

树状数组及其应用

树状数组及其应用(Binary Indexed Trees)一、什么是树状数组【引例】假设有一列数{A i}(1<=i<=n),支持如下两种操作:1.将A k的值加D。

(k,D是输入的数)2.输出A s+A s+1+…+A t(s,t都是输入的数,s<=t)分析一:线段树建立一颗线段树(线段长度1~n)。

一开始所有结点的count值等于0。

对于操作1,如果把A k的值加D,则把所有覆盖了A k的线段的count值加D。

只有log2n 条线段会受到影响,因此时间复杂度是O(log2n)。

每条线段[x..y]的count值实际上就是A x+A x+1+…+A y的值。

对于操作2,实际上就是把[s..t]这条线段分解成为线段树中的结点线段,然后把所有的结点线段的count值相加。

该操作(ADD操作)在上一讲线段树中已介绍。

时间复杂度为O (log2n)。

分析二:树状数组树状数组是一种特殊的数据结构,这种数据结构的时空复杂度和线段树相似,但是它的系数要小得多。

增加数组C,其中C[i]=a[i-2^k+1]+……+a[i](k为i在二进制形式下末尾0的个数)。

由c数组的定义可以得出:i K1(1)201-2^0+1=1…1c[1]=a[1]2(10)212-2^1+1=1…2c[2]=a[1]+a[2]=c[1]+a[2]3(11)203-2^0+1=3…3c[3]=a[3]4(100)224-2^2+1=1…4c[4]=a[1]+a[2]+a[3]+a[4]=c[2]+c[3]+a[4] 5(101)205-2^0+1=5…5c[5]=a[5]6(110)216-2^1+1=5…6c[6]=a[5]+a[6]=c[5]+a[6]………………为了对树状数组有个形象的认识,我们先看下面这张图。

如图所示,红色矩形表示的数组C[]就是树状数组。

我们也不难发现,这个k就是该节点在树中的高度,因而这个树的高度不会超过logn。

树状数组简单易懂的详解

树状数组简单易懂的详解

树状数组简单易懂的详解1 什么是树状数组树状数组(Fenwick Tree),也叫做树形数组,是一种支持单点修改、区间查询的数据结构。

这种数据结构可以在O(log n)的时间复杂度内实现单点修改和区间查询,相较于线段树,树状数组实现较为简单。

2 树状数组的实现原理树状数组的核心思想是利用二进制中的位运算来维护前缀区间和。

假设有一个数组a[],其前缀和为sum[],那么对于每个下标i,只需要维护[1,i]这个区间的和,即sum[i]。

这样,当需要查询区间[1,r]的和时,只需要计算sum[r]即可,不需要遍历每一个元素。

对于单点修改操作,假设我们要将数组a[]的第i个元素修改为x,我们只需要将第i个元素的值改为x,并且要将[1,i]这个区间内所有的sum数组的值都加上x-a[i],即将差值x-a[i]加到了[1,i]区间中。

3 树状数组的实现细节3.1 树状数组的初始化树状数组需要初始化为0,因为我们需要维护的是每个元素的前缀和,如果没有初始化为0,其前缀和也就变得不可预测了。

查询区间[1,r]的和时,只需要计算sum[r]即可。

具体实现过程为:从r开始向前跳,每一个位置的前缀和都加到一个答案变量ans中,直到跳到0为止。

可以使用以下的代码实现:```C++int query(int x){int ans = 0;while(x > 0){ans += tree[x];x -= lowbit(x);}return ans;}```修改操作也很简单,只需要将第i个位置的值改为x,然后将[1,i]这个区间内所有的sum数组的值都加上x-a[i],即将差值x-a[i]加到了[1,i]区间中,可以使用以下的代码实现:```C++void update(int x, int val){while(x <= n){tree[x] += val;x += lowbit(x);}}```4 树状数组与线段树的比较相对于线段树,树状数组的实现更为简单,而且更加省空间。

树状数组万能讲义

树状数组万能讲义

[数据结构]树状数组专题做数据结构的题真是异常的能让人感到自己在脱菜的路上迈进啊。

这一次来看看树状数组。

树状数组的基本知识已经被各种大牛和菜鸟讲到烂了,我就不多说了,下面给出基本操作的代码。

假定原数组为a[1..n],树状数组b[1..n],考虑灵活性的需要,代码使用int *a传数组。

#define lowbit(x) ((x)&(-(x)))int sum(int *a,int x){int s=0;for(;x;x-=lowbit(x))s+=a[x];return s;}void update(int *a,int x,int w){for(;x<=n;x+=lowbit(x))a[x]+=w;}sum(x)返回原数组[1,x]的区间和,update(x,w)将原数组下标为x的数加上w。

这两个函数使用O(logn)的时间和O(n)的空间完成单点加减,区间求和的功能。

接下来做一些升级,让树状数组完成区间加减,单点查询的功能。

考虑将原数组差分,令d[i]=a[i]-a[i-1],特别地,d[1]=a[1]。

那么区间[l,r]整体加上k的操作就可以简单地使用d[l]+=k;d[r+1]-=k来完成了。

此时a[i]=d[1]+..+d[i],所以单点查询a[i]实际上就是在求d数组的[1..i]区间和,很容易完成了。

下面再升级一次,完成区间加减,区间求和的功能。

仍然沿用d数组,考虑a数组[1,x]区间和的计算。

d[1]被累加了x次,d[2]被累加了x-1次,...,d[x]被累加了1次。

因此得到sigma(a[i])=sigma{d[i]*(x-i+1)}=sigma{ d[i]*(x+1) -d[i]*i }=(x+1)*sigma(d[i])-sigma(d[i]*i)所以我们再用树状数组维护一个数组d2[i]=d[i]*i,即可完成任务。

POJ 3468就是这个功能的裸题...下面给出代码:// POJ 3468 Using BIT#include <cstdio>const int fin=0,maxn=100010;__int64 a[maxn],b[maxn],c[maxn];int n,m;inline int lowbit(const int &x){return x&(-x);}__int64 query(__int64 *a,int x){__int64 sum=0;while(x){sum+=a[x];x-=lowbit(x);}return sum;}void update(__int64 *a,int x,__int64 w){ while(x<=n){a[x]+=w;x+=lowbit(x);} }int main(){int l,r,i;__int64 ans,w;char ch;if(fin){freopen("t.in","r",stdin);freopen("t.out","w",stdout);}scanf("%d%d",&n,&m);a[0]=0;for(i=1;i<=n;++i){scanf("%I64d",&a[i]);a[i]+=a[i-1];}while(m--){scanf("%c",&ch);while(ch!='Q' && ch!='C')scanf("%c",&ch);if(ch=='Q'){scanf("%d%d",&l,&r);ans=a[r]-a[l-1]+(r+1)*query(b,r)-l*query(b,l-1)-query(c,r)+query(c,l-1);printf("%I64d\n",ans);}else{scanf("%d%d%I64d",&l,&r,&w);update(b,l,w);update(b,r+1,-w);update(c,l,w*l);update(c,r+1,-(r+1)*w);}}if(fin){fclose(stdin); fclose(stdout);}return 0;}====================================一维到二维的分割线=======================================接下来到二维树状数组。

树状数组的操作

树状数组的操作

树状数组的操作树状数组的⼀些基本操作。

树状数组⽀持单点修改和查询区间和的操作,但和线段树不同,它不⽀持区间修改操作(有些题⽬可以将区间修改转化为单点修改,有些则不可以)。

下⾯介绍树状数组的预处理和基本操作。

1.求lowbit(n)上⼀篇博客介绍了lowbit的定义和使⽤定义的基本求法。

但是依据定义求lowbit未免⿇烦。

因⽽,求lowbit需要导⼊公式:lowbit(n)=n&(-n)。

求lowbit函数:int lowbit(int n){//求n的lowbitreturn n&(-n);}2.对某个元素进⾏加法操作根据树状数组的性质,对于任意⼀个结点x,只有结点c[x]及其祖先结点保存的区间和中含a[x],所以我们只需要对x不断向上搜索即可。

代码:void update(int x,int y){//将x位置元素加上yfor(;x<=N;x+=lowbit(x){//依次寻找x的⽗节点c[x]+=y;//进⾏更新}}这⾥在寻找x的⽗节点时运⽤了上⼀篇博客中的性质③(具体的证明⽅法我也不会qwq)。

举例证明:假设我们要对A[2]进⾏加x的操作。

2的lowbit值为2,则第⼀步2变成了4,对C[4]进⾏处理。

4的lowbit值为4,则第⼆步变成了8,对C[8]进⾏处理。

此时已到达循环边界。

再假设我们要对A[5]进⾏加x的操作。

5的lowbit值为1,则第⼀步5变成了6,对C[6]进⾏处理。

6的lowbit值为2,则第⼆步变成了8,对C[8]进⾏处理。

此时已到达循环边界。

3.查询前缀和操作前缀和的求法:求出x的⼆进制表⽰中每个等于1的位,把[1,x]分成logN个⼩区间,⽽每个⼩区间的区间和已保存在数组c中。

代码(我也不会证):int sum(int x){//求1~x的区间和int ans=0;for(;x;x-=lowbit(x);){ans+=c[x];}return ans;}举例证明:假设我们要求A[6]的前缀和。

树状数组详解

树状数组详解

第01讲什么是树状数组?树状数组用来求区间元素和,求一次区间元素和的时间效率为O(logn)。

有些同学会觉得很奇怪。

用一个数组S[i]保存序列A[]的前i个元素和,那么求区间i,j的元素和不就为S[j]-S[i-1],那么时间效率为O(1),岂不是更快?但是,如果题目的A[]会改变呢?例如:我们来定义下列问题:我们有n个盒子。

可能的操作为1.向盒子k添加石块2.查询从盒子i到盒子j总的石块数自然的解法带有对操作1为O(1)而对操作2为O(n)的时间复杂度。

但是用树状数组,对操作1和2的时间复杂度都为O(logn)。

第02讲图解树状数组C[]现在来说明下树状数组是什么东西?假设序列为A[1]~A[8]网络上面都有这个图,但是我将这个图做了2点改进。

(1)图中有一棵满二叉树,满二叉树的每一个结点对应A[]中的一个元素。

(2)C[i]为A[i]对应的那一列的最高的节点。

现在告诉你:序列C[]就是树状数组。

那么C[]如何求得?C[1]=A[1];C[2]=A[1]+A[2];C[3]=A[3];C[4]=A[1]+A[2]+A[3]+A[4];C[5]=A[5];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];以上只是枚举了所有的情况,那么推广到一般情况,得到一个C[i]的抽象定义:因为A[]中的每个元素对应满二叉树的每个叶子,所以我们干脆把A[]中的每个元素当成叶子,那么:C[i]=C[i]的所有叶子的和。

现在不得不引出关于二进制的一个规律:先仔细看下图:将十进制化成二进制,然后观察这些二进制数最右边1的位置:1 --> 000000012 --> 000000103 --> 000000114 --> 000001005 --> 000001016 --> 000001107 --> 000001118 --> 000010001的位置其实从我画的满二叉树中就可以看出来。

树状数组

树状数组

2 区间更新,单点求和
举个具体的栗子。。。
这道题目就是
意思挺明显的。。。 就是更改,查询。。。。
再举个特别的栗子。。。

求逆序数~

例如在序列 { 2, 4, 3, 1 } 中,逆序依次为 (2,1), (4,3), (4,1), (3,1),因此该序列的逆序数为 4。
所以如何求一个数组中逆序数的个数呢。。

//而且是用树状数组的方法。。。


例如 1 5 2 4 3
从输入顺序考虑: 输入1 ,无逆序;


输入 5, 无逆序;
输入 2,有逆序,因为前面有个5,有一个逆序对,这个逆序 对也可以这样考虑,此刻2所在的位置为3,而此刻小于等于2 的数有2 个,所以3-2=1; 输入 4,有逆序,同上,4-3=1; 输入 3 ,有逆序,同上,5-3=2; 所以~ ans=1+1+2~~~
3 树状数组的两类操作 1 单点更新,区间求和 1 一维树状数组,单点更新,区间求和 比如要更新点x ,x点的值加上val即调用add(x , val) , 求区间[1 , x]的和即为getSum(x) int lowbit(int x) { return x&(-x); } int getSum(int x){ int sum = 0; while(x){ sum += treeNum[x]; x -= lowbit(x); } return sum; } void add(int x , int val){ while(x <=N){ treeNum[x] += val; x += lowbit(x); } }
问题的提出
有一个一维数组,长度为n。

史上最易懂的树状数组入门教程共21页文档

史上最易懂的树状数组入门教程共21页文档

3.每个ei包含的元素数目(包括ai在内)为i的二进制表示中末位连续的0的个 数
为什么这样安排呢? 由主讲人来分析一下它的效率
1.对应二进制数的最末连续0的个数如何 得知?
2.如何存储?
3.如何在修改时访问需要访问的元素?
4.如何在求和时访问需要访问的元素?
如何实现
位运算!
之后将会说到,在实际了应解更用多中位,运我算们,补需码相要关的不 是最末连续的0的个信数息,而,参是见<最位末运算那入段门”1000” 对应的十进制数(虽然Au二le秘者密显增然补版可>以互推)
时间复杂度:单次修改O(1),单次求和最坏 O(n),总时间复杂度最坏为O(n^2),会超时.
为什么超时呢?因为每次我求和的速度太慢 了,那么我们想到了一个被称为“容斥原理” 的小技巧对求和进行加速,再看看.
方法2:开一个1..100000的数组e,e[i]存储的 是a1+a2+..+ai
要求ai..aj的和,那么就是ej-e(i-1). 显然!这下,单次求和的复杂度就变成了O(1)!
当我们求的信息时如果包含的不是的全部信息比如就需要再 找一个显然累加起来这个我们称之为的前驱举个例子
前驱的编号即为比自己小的最近的最末连续比自己多的数 所以前驱=x-lowbit(x),详情参见主讲人
至此我们的树状数组就把大意讲解完毕 了~
可能你会问,如何建立树状数组?
模 就板当作树状数组一开始都是空的,不停的
存储e数组即可,无需存储a数组.
1.对应二进制数的最末连续0的个数如何 得知?
2.如何存储?
3.如何在修改时访问需要访问的元素?
4.如何在求和时访问需要访问的元素?
如何实现

树状数组和线段树-电脑资料

树状数组和线段树-电脑资料

树状数组和线段树-电脑资料一、树状数组在解题过程中,我们有时需要维护一个数组的前缀和S[i]=A[1]+A[2]+...+A[i],实现:对于正整数x,定义lowbit(x)为x的二进制表达式中最右边的1所对应的值。

Lowbit(x)=xand-x对于节点i,如果它是左子节点,其父节点为i+lowbit(i);构造一个辅助数组C,其中Ci=Ai-lowbit(i)+1+Ai-lowbit(i)+2+…+Ai即C的每个元素都是A数组中的一段连续和。

具体是每个灰色节点i都属于一个以它自身结尾的水平长条,这个长条中的数之和就是Ci。

如C12=A9+A10+A11+A12=C10+A11+A12如何更新C数组中的元素:如果修改了一个Ai,需要更新C数组中的哪些元素呢?从Ci开始往右走,边走边“往上爬”(不一定沿着树中的边爬),沿途修改所有结点对应的Ci即可。

预处理时先把C数组清空,然后执行n次add操作。

总时间为O(nlogn)如何计算前缀和Si:顺着结点i往左走,边走边“往上爬”(不一定沿着树中的边爬),把沿途经过的Ci累加起来就可以了。

对于query(L,R)=SR-SL-1。

代码:varb,c,f:array[0..100000]oflongint;ff,a:Array[0..100000]ofboolean; i,j,m,n,x,y,ans,l,r,tmp:longint; s:string;functiondfs(x:longint):longint; beginifx<=1thenexit;c[f[x]]:=c[f[x]]+c[x];dfs(f[x]);end;proceduredfs1(x:longint);begindec(c[x]);ifx<=1thenexit;dfs1(f[x]);end;proceduredfs2(x:longint);begininc(c[x]);ifx<=1thenexit;dfs2(f[x]);beginreadln(n);fillchar(ff,sizeof(ff),true); fori:=1ton-1dobeginreadln(x,y);f[y]:=x;inc(b[x]);end;fori:=1tondoc[i]:=1;fori:=1tondobeginifb[i]=0thendfs(i);end;readln(m);fori:=1tomdobeginx:=0;readln(s);ifs[1]='Q'thenforj:=3tolength(s)dox:=x*10+ord(s[j])-ord('0');writeln(c[x]);end;ifs[1]='C'thenbeginforj:=3tolength(s)dox:=x*10+ord(s[j])-ord('0');ifff[x]thendfs1(x)elsedfs2(x);ff[x]:=notff[x];end;end;End.二、线段树1,.线段树的结构:区间:用一对数a和b表示一个前闭后开的区间[a,b)。

树状数组

树状数组

树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1..n],那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。

来观察这个图:令这棵树的结点编号为C1,。

令每个结点的值为这棵树的值的总和,那么容易发现:C1 = A1C2 = A1 + A2C3 = A3 C4 = A1 + A2 + A3 + A4C5 = A5C6 = A5 + A6C7 = A7C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8...C16 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 + A9 + A10 + A11 + A12 + A13 + A14 + A15 + A16这里有一个有趣的性质:设节点编号为x,那么这个节点管辖的区间为2^k(其中k为x二进制末尾0的个数)个元素。

因为这个区间最后一个元素必然为Ax,所以很明显:Cn = A(n –2^k + 1) + ... + An算这个2^k有一个快捷的办法,定义一个函数如下即可:int lowbit(int x){return x&(x^(x–1));}当想要查询一个SUM(n)(求a[n]的和),可以依据如下算法即可:step1:令sum = 0,转第二步;step2:假如n <= 0,算法结束,返回sum值,否则sum = sum + Cn,转第三步;step3: 令n = n –lowbit(n),转第二步。

可以看出,这个算法就是将这一个个区间的和全部加起来,为什么是效率是log(n)的呢?以下给出证明:n = n –lowbit(n)这一步实际上等价于将n的二进制的最后一个1减去。

而n的二进制里最多有log(n)个1,所以查询效率是log(n)的。

那么修改呢,修改一个节点,必须修改其所有祖先,最坏情况下为修改第一个元素,最多有log(n)的祖先。

树状数组

树状数组

树 状
围内的数据之和.
修改一个位置上数字的值,就是修改一个 叶子结点的值,而当程序由叶子结点返回
数 组
根节点的同时顺便修改掉路径上的结点的 a数组的值.
对于询问的回答,可以直接查找i..j范围
的 运
内的值,遇到分叉时就兵分两路,最后在合 起来.也可以先找出0..i-1的值和0..j的 值,两个值减一减就行了.后者的实际操作

每个星星有个坐标。 如果一个星星的左下方(包含正左和正下)有k颗

星星,就说这颗星星是k级的.比如,在下面的例

图中,星星5是3级的(1,2,4在它左下)。 星星2,4是1级的。例图中有1个0级,2个1级,1

个2级,1个3级的星。



求出各个级别的星星的个数
树 状
算法有很多种,最实用的是树状数 组

如果直接做的话,修改的复杂度

是O(1),询问的复杂度是O(N),M

次询问的复杂度是M*N.M,N的

范围可以有100000以上


用线段树可以这样解:树源自若要维护的序列范围是0..5,先构造下面

的一棵线段树:





可以看出,这棵树的构造用二分便可以实 现.复杂度是2*N.
每个结点用数组a来表示该结点所表示范

while k<=n do

begin

C[k]:=C[k]+delta; k:=k+lowbit(k);
End;
End;
若要求I..j的元素和的值,则求出
1..I-1的值和1..j的值.

树状数组线段树介绍

树状数组线段树介绍


该题为二维树状数组的基本操作,套模板即可。
树状数组例题分析

二维区间查数:如图
树状数组例题分析

例 4(poj 2352 Stars)
天空中有一些星星,这些星星都在不同的位置,每个星星 有个坐标。如果一个星星的左下方(包含正左和正下) 有k颗星星,就说这颗星星是k级的.比如,在下面的例 图中,星星5是3级的(1,2, 4在它左下)。 星星2 ,4是1级的。例图中有1 个0级,2个 1级,1个2 级, 1个3 级的星。 求出各个级别的星星的个数

更新信息规律分析
同样整数n表示为二进制形式 如: 5 101 c[5],c[6],c[8],…… 6 110 c[6],c[8],c[16], …… 7 111 c[7],c[8],c[16], …… …… 规律:n每次增加最低位的1,增到最大为止 结论:更新复杂度 O(log n)

树状数组基本操作
树状数组简单介绍
问题引入

已知一个数组a[],要求完成一下的操作: 1、第i个元素add m; 2、区间【x, y】内的每个元素add m; 3、查询区间【x, y】内所有元素的sum; . . .
基于以上问题设计数据结构

设计一:基于简单数据结构a[] 复杂度分析: O(1), O(n), O(n)…… 设计二:基于简单数据结构sum[] 复杂度分析: O(n), O(n), O(1)……

树状数组简单介绍
建立 c[],其中 C[i]=a[i-2k+1]+……+a[i](k 为i在二进 制形式下末尾0的个数)。 例如: c[1]=a[1] c[2]=a[1]+a[2]=c[1]+a[2] c[3]=a[3] c[4]=a[1]+a[2]+a[3]+a[4]=c[2]+c[3]+a[4] c[5]=a[5] c[6]=a[5]+a[6]=c[5]+a[6] ………………

树状数组

树状数组

树状数组failedbamboo@维护前缀和•包含n个元素的整数数组A,每次可以–C(i, j): 修改一个元素A[i] = j–Q(i): 询问前缀S i=A1+A2+…+A i的值•如何设计算法,使得修改和询问操作的时间复杂度尽量低?例1. 手机(IOI2001)•Tampere地区被划分成N*N个单元,形成一个N*N的表格,行列坐标均为0到N-1,每个单元中有一个移动电话信号发射基地–C(X,Y,A): 基地(X,Y)移动电话数的变化A–Q(L,T,R,B): 询问矩形区域内移动电话总数•注意C操作中A可正可负分析•任意矩形转化为四个前缀矩形•转化为二维前缀和•二维独立, 分别处理, 每次操作log2n例2. 01矩阵•给n*n的01矩阵,支持–C(x0,y0,x1,y1):改变矩形(每个元素取反)–Q(x,y):查询(x,y)的值分析•构造辅助01矩阵C’,初始为0•矩形分解: C(x 0,y 0,x 1,y 1)等价为改变以下4点的值–C’(x 0,y 0), C’(x 0,y 1), C’(x 1,y 0), C’(x 1,y 1)•元素(x,y)的最终值完全取决于在C’中(x,y)的右下方的元素和的奇偶性•维护二维前缀和Æ二维数状数组例3. 方格问题•在一个N*N格点方阵的给了一些长度为1的线段, 每条线段的格式为(X , Y , D), 其中D 可以是H(往右)或者V(往下)•计算出一共有多少个正方形。

上图共有3个正方形,两个边长为1,一个边长为2分析•用一个01矩阵表示每条元线段是否存在•算法一:然后枚举左上角顶点和边长,检查每条元线段是否都存在。

时间复杂度O(n4)•算法二:预处理计算出每个点往下,往右可以延伸多长,判断整条边降为O(1),总时间降为O(n3)•设左上角为(x, y), 若存在边长为K的正方形–不一定存在K-1的正方形–但是我们至少可以确定,K-1正方形的其中两条边是必然存在的算法•依次处理各条斜线, 每条线自左上到右下依次考虑各个点–累加新点往左上方向“作用范围”内的点数–删除往右下方向已经延伸不到当前位置的点•用树状数组维护连续和,O(n2logn)例4. 队伍选择(Balkan 2004)•IOI要来了,BP队要选择最好的选手去参加。

树状数组的算法原理

树状数组的算法原理

树状数组的算法原理宝子!今天咱来唠唠树状数组这个超酷的东西。

树状数组啊,就像是一个魔法小助手,能帮我们快速解决好多关于数组的问题呢。

你想啊,要是有一个长长的数组,里面有好多好多数字,我们要对它做一些统计操作,比如说求某个区间的和之类的,要是用普通的方法,那可就太麻烦啦。

咱先说说树状数组的结构。

它看起来有点像一棵树,但又不是那种特别复杂的树。

它的每个节点都和数组里的元素有着神秘的联系。

你可以把它想象成是一群小精灵在守护着数组。

这些小精灵们可不是乱站的,它们有着自己独特的排列方式。

比如说,对于一个普通的数组,树状数组的节点是怎么对应的呢?树状数组的每个节点实际上是对应着原数组的一段区间。

这就很神奇啦,就好像每个小精灵都负责看守数组里的一块小天地。

而且呢,这些区间还有重叠的部分,就像小精灵们的地盘有时候会有交叉一样。

那它是怎么构建的呢?这就像是搭积木一样。

我们从最基础的元素开始,一点点地构建出这个树状结构。

最开始的时候,每个元素就像是一个小种子,然后随着构建的过程,它们慢慢组合起来,形成更大的节点。

这个过程其实并不复杂,只要按照一定的规则,就像按照一个小魔法咒语一样,就能轻松构建出来。

再说说它的查询操作。

这可是树状数组的拿手好戏。

当我们想要知道某个区间的和的时候,树状数组就像一个超级计算器一样,迅速给出答案。

它不是傻乎乎地一个一个数字去加哦,而是利用它的树状结构,巧妙地跳跃式计算。

就好比我们要找从第3个元素到第7个元素的和,树状数组不会从3加到7,而是根据它的节点关系,快速地找到对应的几个节点,把它们的值加起来就好啦。

这速度,就像闪电一样快。

还有更新操作呢。

如果我们改变了原数组里的一个元素的值,树状数组也能很快地做出调整。

它不会把整个树状结构都重新构建一遍,而是像一个聪明的小工匠,只对受到影响的部分进行修改。

这就像在一个大城堡里,有一块小砖头坏了,工匠只需要修补那一块,而不是把整个城堡都拆了重建。

树状数组在很多实际的问题里都特别有用。

树状数组

树状数组

树状数组先看一个简单的例子:例一:一个学校有n个班级,起初每个班级都没有学生,学校的招生办遇到了一个棘手的问题,他们必须随时了解前n个班级的总人数(任务一),并且随时会增加若干人到某一个班级(任务二)。

我们可以很容易的想到一个简单的算法,开一个长度为n的一位数组记录每个班的人数,这样完成任务二的时间复杂度是o(1),但是完成任务一的时间复杂度是o(n),所以速度较慢。

我们也可以用a[i]记录前i个班级的总人数,这样完成任务一的时间复杂度是o(1),但是完成任务二的时间复杂度是o(n)。

以上两种方法在n很大的情况下速度都比较慢。

所以我们必须引入新的数据结构——数状数组。

我们同样以例一为例,定义数组a,其中a[i]表示第i个班级的人数。

再定义一个数组c,其中C[i]=a[i-2k+1]+……+a[i](k为i在二进制形式下末尾0的个数)。

由c数组的定义可以得出:c[1]=a[1]c[2]=a[1]+a[2]=c[1]+a[2]c[3]=a[3]c[4]=a[1]+a[2]+a[3]+a[4]=c[2]+c[3]+a[4]c[5]=a[5]c[6]=a[5]+a[6]=c[5]+a[6]………………如上图所示为n=9时的情况。

任务一:定理若a[k]所牵动的序列为C[p1],C[p2]……C[p m]。

则p1=k,而p i+1=p i+2li(l i为p i在二进制中末尾0的个数)。

由此得出更改元素值的方法:若将x添加到a[k],则c数组中c[p1]、c[p2]、…、c[p m](p m≤n<p m+1)受其影响,亦应该添加x。

例如a[1]……a[9]中,a[3] 添加x;p1=k=3 p2=3+20=4p3=4+22=8 p4=8+23=16>9由此得出,c[3]、c[4]、c[8]亦应该添加x。

引理若a[k]所牵动的序列为C[p1],C[p2] …… C[p m],且p1 <p2 < …… <p m ,则有l1 <l2< …… <l m(l i为p i在二进制中末尾0的个数)。

数据结构之树状数组

数据结构之树状数组

如果,我们要分析的是“改段求点”型,即
修改某一个区间,使其每个值都加上值val,然后
再去询问你某个点的值是多少。
问题的引入:
给定n个数,a[1]..a[n]。每次我们可能有
两种操作:
(1)将a[i]..a[j]的每个数都加上一个值;
(2)查询a[x]的值。
n的规模如果比较大(约100000)
该如何高效的实现?
令del[i]表示i..MN段被加了多少。则区间修
改[a,b]时,我们让del[a]++,del[b+1]--即可。
(此时树状数组是建立在del数组基础上的)
这个时候,要求某一个位置x的数的值时,用
Getsum(x)来表示。
(即a[x]=sigma(del[1]..del[x]))
问题的深入: 现在,我们如果可以对原数组中,进行区间 的修改,想了解区间的和,该如何去做? del[i]依旧表示i..MN段被加了多少。我们 想知道区间[L,R]的和是多少。这个可以用减法, 用[1,R]的和减去[1,L-1]的和。 提示:建立两棵树状数组
x+lowbit(x)为x的直接父亲编号 void update(int x,int v){ //在x处增加v for (; x<=MN; x+=x&(-x)) C[x]+=v; } //MN为树状数组C的上限
上面为更新C数组的函数
于是乎,最前面提到的问题就完美解决了。 初始化C数组全为0,然后针对每次操作, 操作(1):update(x, val); 操作(2):getsum(x)。 树状数组时间复杂度O(nlgn) ,简洁高效! 学完了树状数组的基本操作,下面来看看
树状数组可以发挥的强大作用吧!

树状数组

树状数组

一搞懂树状数组引用请注明出处:/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图。

树状数组简介

树状数组简介

树状数组简介江苏省常州高级中学曹文一、前言我们平日里总是会遇到一些动态的求和问题,一些朴素的方法时间速度上很难让人满意,因此我们需要设计一些算法来解决所遇到的问题。

1、 基本问题我们下面设法解决这个动态求和问题。

我们定义 N 个整型变量组成的数组 v[1..N],每次操作有两种:(1) 将v[idx] 增加 det(2) 求 ∑=idxi i v 1][2、 朴素解决朴素的解决这个问题,我们用数组记录当前的数值,然后依次求和。

那么操作(1)的复杂度是 O(1),操作(2)的复杂度是 O(idx),令询问数为 Q ,则在最坏情况下为 O(QN)。

这样的复杂度非常不理想,我们要对之优化!二、倍增,分割和优化很多时候在处理区间问题的时候,倍增思想都是非常常用的,比如 RMQ 和LCA 问题,而分割方法也是一个有用的工具,比如线段树。

我们知道线段树是能够解决上面这个基本问题的,但是考虑到线段树的代码比较复杂,这里我们讨论更轻松的方法。

1、 第一个想法我们发现,在朴素算法中,操作(2)用了很多的时间,我们试着优化这个操作的算法。

参照RMQ 的思想,我们可以将一个区间分成一些长度为2的整次幂的小区间。

我们定义f[i][j] 表示区间 [I, i+2^j-1] 的和∑=-+jk k i v 21]1[,那么如果我们能够得到所有 f[i][j] ,那么我们就能够在 O(log N) 的时间里计算出我们需要的和。

具体方法如下:对于区间 [L,R],我们找到最大的 p ,使得 2^p <= R-L+1,而 sum[L,L+2^p-1] =f[L][p],于是我们只要继续计算 [L+2^p,R]就可以了。

由于每次找最大的 p,因次至多计算 log N 次就能得到结果——这就是倍增思想的应用。

但是很不幸,我们再观察操作(1)的时候,我们发现由于和某一个位置相关的 f[i][j] 实在太多了,所以操作(1)的时间就花的过多了,问题并没有得到解决。

树状数组详解

树状数组详解

树状数组详解在我们考虑到要对⼀个区间进⾏操作的时候,第⼀解法就是想到运⽤暴⼒⼤法,但是难免会⾯临着超时的危险。

有没有⼀种⽅法可以很好的解决这种区间操作呢?⾸先先考虑⼀下这三个问题:问题⼀:(1)有⼀个机器,⽀持两种操作,在区间[1,10000]上进⾏。

操作A:把位置x的值+k操作B:询问区间[l,r]所有数字之和区间的初始值全部为0现在你要充当这个机器,操作A和操作B会被穿插着安排给你,要求对于所有操作B,给出正确的答案。

怎样做才能最节省精⼒?问题⼆:(2)有⼀个机器,⽀持两种操作,在区间[1,10000]上进⾏。

操作A:把区间[l,r]的值全都+x操作B:询问位置x的值。

区间的初始值全部为0现在你要充当这个机器,操作A和操作B会被穿插着安排给你,要求对于所有操作B,给出正确的答案。

怎样做才能最节省精⼒?问题三:(3)有⼀个机器,⽀持两种操作,在区间[1,10000]上进⾏。

操作A:把区间[l,r]的值全都+x操作B:询问区间[l,r]所有数字之和区间的初始值全部为0现在你要充当这个机器,操作A和操作B会被穿插着安排给你,要求对于所有操作B,给出正确的答案。

怎样做才能最节省精⼒?三个问题中操作的数量都可以认为是10000(甚⾄有可能会更⼤)注意:1.举个例⼦,进⾏这种类似的操作:从⼀⾏任意打乱的数字中找⼀个数字不能认为⼀瞬间就可以找到,在这⾥所花费的精⼒和数字的总数具有线性关系。

2.我们认为将数据转换为⼆进制不需要任何时间。

对于问题1,如果我们每种操作都暴⼒进⾏,那么显然总的时间复杂度为O(mA+n*mB),n表⽰区间长度,mA表⽰操作A执⾏的次数,mB表⽰操作B执⾏的次数。

那么有没有⼀种更加轻松的办法呢?我们将引⼊⼀种数据结构,叫做<树状数组>。

⾸先了解⼀下在整个树状数组知识中起到⾮常重要的作⽤的⼀个东西,可所谓是树状数组的核⼼,应⽤的却说很巧妙。

lowbit(x)=x&((~x)+1) (为了少引⼊补码的概念,我们这⾥稍微⿇烦了⼀下,其实x&-x就⾏)它的作⽤是什么呢?它只保留“从低位向⾼位数,第⼀个数字1”作为运算结果⽐如⼆进制数00011100,执⾏这个函数后,结果就是00000100,也就是4。

树状数组知识点详解

树状数组知识点详解

树状数组知识点详解树状数组树状数组是⼀种数据结构,它的作⽤就是优化查询和修改的操作。

试想,我们假如在做⼀道题的时候使⽤裸的⼀维数组来存储数据,那每次区间修改需要O(1)的时间,但查询却需要O(n)的时间,针对于某些题⽬,数据量奇⼤⽆⽐,必然会TLE。

所以我们使⽤树状数组来优化这两个操作,使得修改和查询均可以在O(logn)的时间内完成,提升效率。

(这是百度百科上树状数组的图)可以直观地看出树状数组是个什么模式,是的,这就是⼀棵树,⽽这棵树上每个节点存储的数据就是它所有⼉⼦节点的数据和。

所以我们就可以在树上做修改和区间查询(节点减节点),来做到树状数组这种优化⽅式。

树状数组的实现需要三个特别重要的函数,我⼀般把它们写成fix()(向上修改),getsum()(向下查询),和lowbit()(这个函数是理解树状数组运⾏过程的关键)。

lowbit()函数⾸先从最关键的lowbit函数⼊⼿。

它长这个样⼦:int lowbit(int x){return x+=x&-x;}这⾥还涉及到了位运算的相关知识,所谓x&-x,是在⼆进制上的运算操作,它的实现也很简单,把x按位取反,最后同时上移最后的那个1。

⽐如lowbit(34)最终的结果就是36。

那么这个lowbit函数是⼲什么⽤的呢?关于lowbit运算,有不太了解的⼩伙伴可以参考本蒟蒻的这篇博客:我们可以把它理解成对树状数组的遍历⽅式。

根据树状数组的⽰意图可以发现,我们如果想对原数组进⾏元素修改,会牵连到于之关联的树状数组整个链。

所以我们必须层层向上修改,每⼀层都要修改,才能保证树状数组存储的元素的正确性。

那么这个层层向上(向下),就需要lowbit这个函数,或者是说这个功能,来实现。

fix()函数void fix(int x){for(int i=x;i<=n;i+=i&-i)c[i]++;}void fix(int x,int y)//表⽰在x元素处修改y个单位{for(int i=x;i<=n;i+=i&i)c[i]+=y;}通过刚才学习lowbit函数,我们应该可以理解这个循环的含义。

树状数组 详解

树状数组 详解

树状数组详解对于普通数组,其修改的时间复杂度位O(1),而求数组中某一段的数值和的时间复杂度为O(n),因此对于n的值过大的情况,普通数组的时间复杂度我们是接受不了的。

在此,我们引入了树状数组的数据结构,它能在O(logn)内对数组的值进行修改和查询某一段数值的和。

树状数组是一个查询和修改复杂度都为log(n)的数据结构,假设数组a[1..n],那么查询a[1]+...+a[n]的时间是log级别的,而且是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为log级别。

假设A[]数组为存储原来的值得数组,C[]为树状数组。

我们定义:C[i] = A[i - 2^k + 1] + ..... + A[i] 其中k为i用二进制表示时的末尾0的个数。

例如:i= 10100,则k = 2,i = 11000,则k = 3;为了方便我直接写的是二进制的i,请读者注意。

此时我们可以知道,C[i] 它里面包含了2^k个A[]元素,这2^k个元素是从C[i]往后一直递减的2^k个元素,即i 一直减小的。

其中我们有一种快速的求解2^k的值得方法:[cpp]view plaincopyprint?利用机器的补码原理也可以写成这样:[cpp]view plaincopyprint?下面我们还要求的就是如何快速的修改某一个元素的值以及求出某一段元素值的和;先来看它是怎么快速修改某一个元素的值的:我们举个例子(为了方便i的值直接写成二进制了):i = 11000,此时k = 3;这2^k = 3 个数即为:A[11000],A[10111],A[10110],A[10101],A[10100],A[10011],A[10010],A[10001] C[11000] = A[11000]+A[10111]+A[10110]+A[10101]+A[10100]+A[10011]+A[10010]+A[10001];这里我们会发现:A[10100] + A[10011] + A[10010] + A[10001] = C[10100]而A[10010] + A[10001] = C[10010]A[10011] = C[10011]所以C[10100] = C[10010] + C[10011] + A[10100]又A[10110] + A[10101] = C[10110]而A[10101] = C[10101]所以C[10110] = C[10101] + A[10110]又A[10111] = C[10111]至此我们可以得出:C[11000] = C[10100] + C[10110] + C[10111] + A[11000]到这里我们可以得出:k的值就表示子树的个数,子树即为树状数组的元素。

  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

10100111
00001111
这是x-1
这是x xor (x-1)
00001000
这是(x xor (x-1)) and x
-x的那个之所以可以是因为-x= (not x)+1
1.对应二进制数的最末连续0的个数如何得知?
2.如何存储? 3.如何在修改时访问需要访问的元素? 4.如何在求和时访问需要访问的元素?
如何实现
一维数组!
树状数组有着明显的类似树一样的逻辑关 系,让我们感觉应该使用树来存,可是事实上 一维数组足矣!
那你可能会问我,如何访问父亲和兄弟?
由于某元素和其父亲,最近的兄弟在下标上 存在关系(与lowbit(x)有关),利用这个关系就 可以用一维数组存储了,而且稍后你会发现, 存储e数组即可,无需存储a数组.
Procedure change(x,delta:shu); Begin While x<=n do Begin e[x] := e[x]+delta; x := x+lowbit(x); End; End;
Function sum(x:shu):shu;
Function lowbit(x:shu):shu; Begin lowbit := (-x) and x; End; Begin s := 0; While x>0 do Begin s := s+e[x]; x := x-lowbit(x); End; Var s:shu;
sum := s;
End;
推荐习题:
POJ 2299
POJ 2352 欢迎交流讨论,指出不足之处!
谢谢!
注意观察:
1.每个元素至多仅被一个元素包含,这点和树有很大相同,但整体并不是树
2.每个ei可认为是仅包含ai和其它若干个e元素 3.每个ei包含的元素数目(包括ai在内)为i的二进制表示中末位连续的0的个数 为什么这样安排呢? 由主讲人来分析一下它的效率
1.对应二进制数的最末连续0的个数如何得知?
前驱的编号即为比自己小的, 最近的, 最末连续0 比自己多的数
所以前驱=x-lowbit(x),详情参见主讲人
至此我们的树状数组就把大意讲解完毕了~ 可能你会问,如何建立树状数组? 就当作树状数组一开始都是空的,不停的用修改操作修改就可以了!
模板
Const ln=100000; Type shu=longint; sz=array [1..ln] of shu; Var n:longint; e:sz;
树状数组
包九中信息学竞赛小组 Aule
我们先来看一个问题
这里有n个数,姑且命名为a1,a2…an 也可以当作数组a有n个元素. 实现两种操作: 修改某个数的值 求出某段数的和(如a3+a4+..+a10+a11) n<=100000,总操作数<=100000
怎么做?
方法1:开一个1..100000的数组,题目让我干 啥我干啥. 时间复杂度:单次修改O(1),单次求和最坏 O(n),总时间复杂度最坏为O(n^2),会超时. 为什么超时呢?因为每次我求和的速度太慢 了,那么我们想到了一个被称为“容斥原理” 的小技巧对求和进行加速,再看看.
1.对应二进制数的最末连续0的个数如何得知?
2.如何存储? 3.如何在修改时访问需要访问的元素? 4.如何在求和时访问需要访问的元素?
如何实现
修改操作
事实上修改操作只涉及到父节点的访问
经过观察和探究,前人们得出了这个规律: 父亲:比他大的,离他最近的,末位连续0比他多的数就是他 的父亲,X节点父亲的编号=x+lowbit(x)
2.如何存储? 3.如何在修改时访问需要访问的元素? 4.如何在求和时访问需要访问的元素?
如何实现
位运算!
之后将会说到,在实际应用中,我们需要的不 了解更多位运算,补码相 是最末连续的0的个数 ,而是最末那段 ”1000” 关信息 ,参见<位运算入门 Aule秘密增补版> 对应的十进制数(虽然二者显然可以互推)
当我们求的 1 . x 信息时如 , e [ x ] 果包含的不是的 1 . x 全部信息比 , ( 如就 e [ 6 ] = a 5 + ) 需要再 找一个e 显然k ( ] k [ 累加起来, ) x < 这个k 我们称之为x 的前驱, 举个例子:
a 7 , 4 e = 6 . 2 + ] 1 [
方法2:开一个1..100000的数组e,e[i]存储的 是a1+a2+..+ai 要求ai..aj的和,那么就是ej-e(i-1).
显然!这下,单次求和的复杂度就变成了O(1)! 哈哈哈哈哈哈哈哈哈啊哈
但是单次修改的最坏复杂度变成了 O(n).........囧..总复杂度又变成了O(n^2) 看看我们这两次失败的尝试,原因在哪里呢?
引入新形式!
这里引入一种新的存储方式
每个ei存储的a元素数目不是一开始规定好 的,而是根据i的不同而不同的 进而达到什么目的呢?树一样形状的存储.
下面看下形象的解释
方格中数字代表对应数组的第几个元素,下排是a数组,其上方的是e数组,最下 的二进制则是对应编号的二进制表示.
箭头表示这个数组元素被哪个数组元素包含了,比如e[2]=e[1]+a[2]=a[1]+a[2], e[4]=e[2]+e[3]+e[4]=e[2]+a[3]+a[4]=a[1]+a[2]+a[3]+a[4].
请大家研究一下这段求”1000”的伪代码:
lowbit(x) := (((x-1) xor x) and x);
如果你对补码有所了解,还可以看看这个:
lowbit(x) := ((-x) and x);
上面的算法原理,就是利用最后这段”10000” 的性质,比如对于”10101000”求最末100 10101000 这是x
原!因!就!是!
第一次我们数组元素ai存储的信息只包含那 个ai,管的太少了!所以求和起来慢 第二次我们数组元素ei存储的信息包含了 a1,a2..ai,管的太多了...这样会导致修改a值 的时候涉及到的元素太多了
容易想到,我们如果能想到一种方法,让数组 元素存储的a的数目适当多,就可以的最末连续0的个数如何得知?
2.如何存储? 3.如何在修改时访问需要访问的元素? 4.如何在求和时访问需要访问的元素?
如何实现
求和操作
首先要意识到,想求ei..ej的和,只需求出 1..e(i-1)和1..ej的和就可以了.所以我们研究 如何求1..ex
相关文档
最新文档