树状数组(详细讲解)

文章介绍了树状数组这一数据结构,包括它的概念、优点和基本操作,如change和sum。通过例题解析展示了树状数组在解决数组操作问题上的高效性,如数星星(PKU2352)和矩阵(PKU2155)问题。文章强调树状数组在代码简洁性和时间复杂度上的优势,以及其在算法竞赛和数据结构学习中的重要性。
摘要由CSDN通过智能技术生成

目录

【引言】

【树状数组介绍】

【对于树状数组的几个问题】

【例题】

一、数星星(pku 2352)

二、matrix(pku 2155)

三、mobile phone(pku 1195)

四、郁闷的出纳员(noi 2004)

五、复杂的按钮

【总结】


【引言】

先由一道题目引人(线段树的入门题目)
题目大意:给出一个数组A,有以下操作:
Change(i , data):把A[i]的值加上data
Sum(i , j):求区间[i , j]的和,并输出。
题目分析:很容易就想到,直接模拟,开一个数组,改变值时就直接操作。求和时就把从i到j的值加起来,输出。
     但这种直观的做法显然十分不足,虽然对于change的操作很方便,但对于sum的操作就会消耗很多的时间。由于本题操作较多,所以这种做法在数据规模较大时就会超时。
     所以,就要求一种高效的算法。而线段树就能满足这种要求,用线段树的做法在这里就不详谈了(有兴趣的可以自己想)。用线段树做的话,虽然change的操作的耗时升到了LogN,但sum的操作的耗时却同样降到了LogN,时间消耗分摊后就很低,是能够接受的。
    在这里,要介绍另一种做法——树状数组。
    树状数组是一类比较简单的数据结构,和线段树比较像。树状数组是维护前缀和的一种数据结构。这就导致它能用较短的时间来实现查询和改变值。
树状数组比线段树、平衡树要容易写,代码复杂度低。但不足的是适用面较窄(空间消耗大)。有句话是这样说的:能用树状数组的就能用线段树,能用线段树的就能用平衡树。
所以一般都是能用线段树就不用平衡树,能用树状数组就不用线段树。(从这其实就可以知道树状数组的魅力有多大了)
   


【树状数组介绍】


首先,先要理解树状数组的含义。含义其实就是用一个数组,构成树形结构来维护原数组的前缀和。

观察下图(这个图也是网上经常出现的):

显然,对于树状数组C,C[I]对应“管辖”多少个元素,与它对应二进制数最右端第一个1的位置有关。这样,就能够达到询问一个区间的值,或者改变值的时间代价为LogN。


【对于树状数组的几个问题】


怎么查找一个数二进制数最右端第一个1的位置
很容易就想到一种方法就是:  
  LowBit(x)=x and ((not x )+1)。
对于数x,not x后就会对x的二进制数的每一位取反。+1后,改变的二进制数最右端第一个0就会变成1,而后面的1就会变回0。但这个操作不改变前面的变化,所以再进行and操作时,就能够得到二进制数最右端第一个1的位置。
由于not x = -x-1 (这个是因为对于0和-0的补码不同所致),那么求二进制数最右端第一个1的位置的式子就可以写成很简单的形式:
  LowBit(x)=x and –x。

从V的位置改变值data:
很容易就能想到的是,因为树状数组是要维护前缀和,那么改变一个值时,它影响的是后面的值。又由于某个树状数组中的元素是“管辖”一个区间的值;那么,这个值影响的是下一个“管辖区”,这个“管辖区”在哪呢?明显,其实就是v+LowBit(v),然后往后一直修改,到比数组的长度大的时候停下来。
以下为代码(n为树状数组的长度):
procedure add(v,data:integer);
begin
while v< n do
 begin
  inc(c[v],data);
  v:=v+LowBit(v);
 end;
end;
求出1~V的和:
从改变值的那里很容易想到:树状数组是维护前缀和的。那么,记录了和的是前面的元素,那么只要从这个“管辖区”,跳到前面的“管辖区”,把和加起来就是了:
function sum(v:integer):integer;
var i:integer;
begin
 i:=0;
 while v>0 do
 begin
  inc(i,c[v]);
  v:=v-LowBit(v);
 end;
 exit(i);
end;
对于树状数组,就只有这么些函数(所以代码就比较少)。但树状数组的神奇之处就在于这三个函数了。(本质只有两个)仅仅就是这三个函数,但这就是树状数组的看家本领了。用好这三个函数,也就能解决比较多的问题。

那么,对于开头提到的题目,对于change(i , data)的操作,直接就执行add(i , data)就可以了,对于sum(i , j)的操作,就执行sum(j)-sum(i-1)就是了。


【例题】


一、数星星(pku 2352)


题目大意:给出一堆星星的坐标。一个星星的级别的定义为在它左下方星星的个数。输出各个级别星星的个数。
题目解释:
其实这题就是一道比较裸的数据结构题——用线段树能够很好的解决掉。同样,用树状数组也能做出,而且非常漂亮。
由于坐标本来就排好序,就更好做了。只需从左下角开始线性扫描一遍,然后按照星星的x坐标插进树状数组里。
为什么这样呢?好明显,就好像把星星都压缩到一行,在左边的星星个数就好统计了。因为坐标本来就排好序,那么对于一个星星,如果y坐标比它小的,那么肯定会先插到了数组里,那么综合起来,数组记录的就是该星星的左下方星星的个数了。只需要直接一个一个元素插入就是了。
const
  maxn=15000;
  maxm=32001;
var
  x,y:array[1..maxn]of longint;
  c:array[1..maxm]of longint;
  h:array[0..maxn-1]of longint;
  i,n,m:longint;
function lowbit(v:longint):longint;
begin
  lowbit:=v and -v;
end;
procedure insert(x:longint);
var
  y:longint;
begin
  y:=x;
  while y<=m do
  begin
    c[y]:=c[y]+1;
    y:=y+lowbit(y);
  end;
end;
function query(x:longint):longint;
var  ans,y:longint;
begin
  y:=x;
  ans:=0;
  while y>0 do
  begin
    ans:=ans+c[y];
    y:=y-lowbit(y);
  end;
  query:=ans;
end;
begin
  assign(input,'star.in'); reset(input);
  assign(output,'star.out'); rewrite(output);
  readln(n);
  m:=0;
  for i:=1 to n do
  begin
    readln(x[i],y[i]);
    if m< x[i] then m:=x[i];
  end;
  m:=m+1;
  for i:=1 to n do
  begin
    inc(h[query(x[i]+1)]);
    insert(x[i]+1);
  end;
  for i:=0 to n-1 do
    writeln(h[i]);
  close(input); close(output);
end.



二、matrix(pku 2155)


题目大意:给出一个矩阵A,一开始,矩阵中的元素均为0。
现给出两种操作
C(x1,y1,x2,y2),改变子矩阵(x1,y1,x2,y2)的状态。如果本来是0就变为1,如果本来是1就变成0.
Q(x,y),询问A[x,y]的状态。
题目理解:
这道题的难点在于C操作。怎么记录呢?其实别被题目所蒙蔽,题目是说0变1,1变0。其实只要记录一个点执行C操作几次就可以了。由于原来为0,那么执行次数为偶数次,那么状态就为0;奇数次,那就是1。其实只理解到这里,就差不多能做出来了。
同时,这道题涉及的二维,那就要用二维的树状数组(其实就是多加一重循环而已)。附上代码(核心的部分):

while i<=n do
begin
  j:=y1;
  while j<=n do
  begin
    inc(c[i,j]);
    j:=j+LowBit(j);
  end;
  i:=i+LowBit(i);
end;

插入的时候要注意:先考虑,如果插入的是(i , j , n , m),那么,只需直接调用一次插入add(i , j)就可以了。但现在插入的子矩阵是(x1,y1,x2,y2),其实也是一样的,运用容斥原理,就可以写为add(x1,y1);add(x1,y2+1);add(x2+1,y1);add(x2+1,y2+1)。


三、mobile phone(pku 1195)


题目大意:现给出一个矩阵A,初始时矩阵中元素均为0。
现给出两种操作:
1 (x,y,data),把A[x,y]加上值data
2 (x1,y1,x2,y2),询问子矩阵(x1,y1,x2,y2)的元素总和
题目分析:明显,这道题跟例题二十分相似,同样是用二维的树状数组。但不同的是,两个操作交换了。前面一题,询问的是一个元素的值,而这题是询问一个子矩阵值的和;前一题改变的是一个子矩阵的值,而这题改变的是一个元素的值。所以,操作就刚刚反过来就好了。直接插入;在询问值时就要用容斥原理,可以写为sum(x2,y2)-sum(x1-1,y2)-sum(x2,y1-1)+sum(x1-1,y1-1)。

小结:以上的题目均为一维和二维的树状数组。其实只需把add和sum两个函数用好,树状数组就不难了。这两个函数就是树状数组最神奇的地方了。
下面的这个例题就用的了树状数组的另一个本领——找第k大。(平衡树等其他数据结构都有这个功能,树状数组当然也有)


四、郁闷的出纳员(noi 2004)


题目大意:有以下的操作:
     I_k 新建一个工资档案,工资为k
     A_k 把每个员工的工资都加上k
     S_k 把每个员工的工资都减掉k
     F_k 查找第k多的工资
         当某个员工的工资低过一个定值时,那么他就会离开这个公司。
分析题目:这道题可以用平衡树很完美的解决掉,这里主要讨论的是如何用树状数组解决。
         对于那个加减工资的操作,完全就可以用一个变量解决掉,并不需要每次都实际的进行修改(用平衡树的,线段树的都是这样处理的了)。不过,执行减的这个操作时需要进行检查,如果工资过低时就需要减去这个员工——在最低的标准那里,往前进行删减。
         主要的问题其实是怎么找第k大,这个就是这里要详细讲的。明显,由于树状数组是基于二进制数的,那么找第k大就可以用二分逼近(这就是为什么树状数组找第k的效率是LogN的缘故),因为一个大的“管辖区”必定是2的几次方。所以,就有一种很神奇的写法了(这个要好好想想为什么):
Two[i]表示2i的值对应为多少
C为树状数组

function findk(k:integer):integer;
var i,j,a:integer;
begin
 j:=0;a:=0;
 for i:=14 downto 0 do
 begin
  if a+c[j+two[i]]< k then
  begin
    a:=a+c[j+two[i]];
    j:=j+two[i];
  end;
 end;
 exit(j+1);
end;
小结:以上题目都是比较裸的树状数组,这里选用的目的是介绍树状数组的三个重要的函数。(即树状数组的用途)其实树状数组是一种数据结构,那么它只能是一种辅助的用途,实际中是不可能出现这种赤裸裸的题目的,我们要学会如何去在做题时应用上数据结构。
现在选用一个例题来说明这个问题。



五、复杂的按钮


题目大意:现在有一堆开着的按钮,目标是把所有的按钮都关上。
         但按下一个按钮的同时,有一些其他的按钮会弹出(即开了)。要达到目标并且为最小步数,输出按按钮的顺序。同时要求,如果有多种方案,那么输出字典序最小的。如果不可能达到,就输出“no solution”。
题目分析:这道题的做法十分明显,就是构造出一个图,进行拓补排序。因为对于一个按钮,如果按下后,会使另外的按钮弹起,那么这些会弹起的按钮不可能在这个按钮前按下了。但由于其数据范围达到30000。那么很明显,就要用链接链表来储存边。(建议使用模拟链表)这样的话,进行拓补排序的效率就为N了,肯定能过的。
         另外,因为要求最小的字典序。那么,在进行拓补排序时,如果有多个入度为零的点,那么就要选用小的那个。如果仅仅用一个循环,那么查找的效率就会为O(N),程序的总体效率就为O(N2)。这显然是过不了的。那么就可以借助于数据结构,把这个查找时间降为LogN,那么总效率就为O(NLogN),这样就能够过了。至于这种数据结构是啥,你想用什么就什么。平衡树就太难写了,建议使用堆或者树状数组。
这题其实只是略微使用了一下数据结构,一些复杂的题目则会有更高的要求。

【总结】


总结:
很明显的,数据结构就是一种辅助工具,不可能有题目是赤裸裸的数据结构题目。(除了那种练数据结构的题目)数据结构的作用就是为了降低时间的消耗:可用于查询、修改值、维护数组。
      而树状数组可以说一种比较简单的数据结构,但树状数组的比较优秀性在于:代码复杂度低,时间消耗低,功能比较全。所以树状数组是一种性价比较高的数据结构。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值