二分索引树与线段树分析

  二分索引树是一种树状数组,其全名为Binary Indexed Tree。二分索引树可以用作统计作用,用于计某段连续区间中的总和,并且允许我们动态变更区间中存储的值。二分索引树和线段树非常相似,二者都享有相同的O(log2(n))时间复杂度的更新操作和O(log2(n))时间复杂度的查询操作,区别在于二分索引树更加简洁高效,而线段树则较冗杂低效,原因在于对二分索引树的操作中是使用了计算机中整数存储的特性来进行加速,而线段树中由于使用的是比较操作,因此性能不及二分索引树。那么为什么我们不抛弃线段树呢?原因在于所有二分索引树能解决的问题,线段树也都可以解决,但是线段树还能解决许多二分索引树无法解决的问题。下面将先后讨论二分索引树和线段树。



  要叙述二分索引树,我们必须先说明二分索引树高效的关键,lowbit操作,其中lowbit(x)=x&-x。我们知道在计算机中,对于一个带符号n位整数b,其由n个二进制位表示,可以记为(b[n-1],b[n-2],...,b[0]),其中对于任意0<=i<n,b[i]为0或1。而一个这样的向量,其实际表示的整数值为$$ value\left(b\left[n-1\right],\cdots ,b\left[0\right]\right)=-b\left[n-1\right]\cdot 2^{n-1}+\sum_{i=0}^{n-2}{b\left[i\right]\cdot 2^i} $$对于一个任意正整数X,我们先对其按位取反(除了首位),得到一个新的数Y,很容易可以得知X+Y=2^(n-1)-1,因此X+Y+1=2^(n-1),换言之Y+1-2^(n-1)=-X,因此我们得到-X的二进制表示,其后n-1位为X按位取反后加1得到的(与Y+1的后n-1位相同),且首位为1。接下来考虑X&-X的实际含义,注意由于X为非负正数,因此X的首位为0,而运算为且运算,因此我们可以忽略-X的符号位带来的影响。我们不妨认为X的后面k位均为0,且X[k]=1,对应的Y的后面k位均为1,而Y[k]=0。而Y+1的后面k位均为0,而(Y+1)[k]=1,之后的前面n-2-k位与Y一致,与X相反。因此X&-X得到的值应该为2^k,到此我们可以得出一个结论X&-X得到的数为以二进制视角从后数起第一个为1的二进制位所代表的整数值。事实上这个结论对于X为0时也是成立的,并且由于且运算满足交换律,因此当X为负数时也可以得到正确结果,这些只是补充说明而已,我们要用到的只是lowbit应用到正整数情况下表现出的性质而已。

  接下来我们用二分索引树表示一段长度为L的数组A,且从1开始计数,即我们认为下标分别为1,2,...,L。二分索引树支持两种操作,修改某个A[i]的值,以及查询S[j]=A[1]+A[2]+...+A[j]的加总和,两种操作允许以任意次序交错执行。

  不难知道使用原始数组存取,我们的查询时间复杂度将达到O(L),在查询密集的情况下是一个噩梦,而如果我们缓存S[j]的值,这样每次更新的时间复杂度将达到O(L),这也是不允许的。我们可以利用一个精致的机制,我们首先定义一个长度为L的数组data,其中data[i]表示S[i]-S[i-lowbit(i)]的值,换言之,data[i]表示A在区间(i-lowbit[i],i]范围内所有元素的加总值。这样我们的查询操作可以用下面的伪代码实现:

query(x)//查询A[1]+...+A[x]
    sum = 0
    for(i = x; i > 0; i = i - lowbit(i))
        sum = sum + data[i]
    return sum

  我们定义函数f(x):=x-lowbit(x)。

  在整个流程中我们汇总了下标为f^0(x),f^1(x),...,f^k(x)的data中的值,我们对其进行加总得到:$$ data\left[f^0\left(x\right)\right]+data\left[f^1\left(x\right)\right]+\cdots +data\left[f^k\left(x\right)\right] $$ $$ =S\left[f^0\left(x\right)\right]-S\left[f^1\left(x\right)\right]+S\left[f^1\left(x\right)\right]-S\left[f^2\left(x\right)\right]+\cdots +S\left[f^k\left(x\right)\right]-S\left[0\right] $$ $$ =S\left[f^0\left(x\right)\right]=S\left[x\right] $$

  到此我们说明了query返回的结果是正确的,而时间复杂度,我们每次调用函数f都会导致i最靠后的为1的二进制位被修改为0,而i中最多有log2(i)个为1的二进制位,因此时间复杂度为O(log2(i)),在i=L的情况下达到最大O(log2(L))。

  说明完了查询,下面说明如何存储数据。当我们修改了A[i]中的值,我们势必需要修正所有满足f(x)<i<=x的x对应的data[x]中存储的值,以保证data中数据的正确。

  命题1:对于一个给定数k和i,最多只存在一个同时满足lowbit(x)=2^k且f(x)<i<=x的数x。

  证明:假设x与y为两个不同的满足条件的值,则有x-2^k<i<=x与y-2^k<i<=y成立,不妨设x<y,则有y-x>2^k(由于2^k同时是x与y的约数,后面不再说明),而y-2^k>x>i>y-2^k将成立,这是不可能的,因此命题成立。

  命题2:若两个不同的数x,y同时满足f(x)<i<=x,f(y)<i<=y,则由lowbit(x)<lowbit(y)可以得出x<y。

  证明:记2^p=lowbit(x),则x>y可以得出x-y>=2^p,即i>x-2^p>=y,这与前提相悖,因此命题成立。

  命题3:若某个数x满足f(x)<i<=x,且i的后面t个二进制位为0,且i[t]=1,则lowbit(x)>=2^t

  证明:设lowbit(x)=2^p<2^t,我们可以记y为x将后面t位均设置为0后的值,因此有y+2^t>x>=i,即i-y<2^t,从而可得i<=y,而x-2^p>=y>=i,因此命题成立。

  命题4:若某个数x满足f(x)<i<=x,那么y=x+lowbit(x)是大于x中满足f(y)<i<=y的最小下标

  证明:证明y=x+lowbit(x)满足f(y)<i<=y比较简单,这里不证明。而对于任意z,若z>x,且f(z)<i<=z,则根据命题3可知lowbit(z)>lowbit(x),由此可知z-x>=lowbit(x),即z>=lowbit(x)+x=y。

  这里我们定义g(x)=x+lowbit(x)。由上面4个命题可以得出一系列需要更新的下标从小到大排序为g^0(x),g^1(x),...,g^k(x)。因此我们给出下面代码:

update(j, val) //令A[j]增加val
    for(i = j; i <= L; i = i + lowbit(i))
        data[i] = data[i] + val

  这里使用的是加法,和前面提及的直接赋值有所不同,但是可以将直接对A[j]赋值v对应的修改为令A[j]增加v-A[j]即可。

  由于i在每次循环,其lowbit值不断增大,因此update的时间复杂度也是log2(L)。



  线段树有空再补上...

时间: 2024-08-24 09:55:32

二分索引树与线段树分析的相关文章

[BZOJ 1901] Dynamic Rankings 【树状数组套线段树 || 线段树套线段树】

题目链接:BZOJ - 1901 题目分析 树状数组套线段树或线段树套线段树都可以解决这道题. 第一层是区间,第二层是权值. 空间复杂度和时间复杂度均为 O(n log n). 代码 树状数组套线段树 #include <iostream> #include <cstdlib> #include <cstdio> #include <cmath> #include <algorithm> #include <cstring> usin

【vijos】1750 建房子(线段树套线段树+前缀和)

https://vijos.org/p/1750 是不是我想复杂了.... 自己yy了个二维线段树,然后愉快的敲打. 但是wa了两法.......sad 原因是在处理第二维的更新出现了个小问题,sad. void pushup1(int x) { for1(i, 1, mm<<2) mn[x][i]=min(mn[lc][i], mn[rc][i]); } 这里注意是mm*4...我该好好想想了..这是在dbg的时候找出来的问题.sad. 我觉得很奇怪,线段树的底层节点一共就mm个,那么整棵树

【bzoj4785】[Zjoi2017]树状数组 线段树套线段树

题目描述 漆黑的晚上,九条可怜躺在床上辗转反侧.难以入眠的她想起了若干年前她的一次悲惨的OI 比赛经历.那是一道基础的树状数组题.给出一个长度为 n 的数组 A,初始值都为 0,接下来进行 m 次操作,操作有两种: 1 x,表示将 Ax 变成 (Ax + 1) mod 2. 2 l r,表示询问 sigma(Ai) mod 2,L<=i<=r 尽管那个时候的可怜非常的 simple,但是她还是发现这题可以用树状数组做.当时非常young 的她写了如下的算法: 1: function Add(x

UVALive 7148 LRIP【树分治+线段树】

题意就是要求一棵树上的最长不下降序列,同时不下降序列的最小值与最大值不超过D. 做法是树分治+线段树,假设树根是x,y是其当前需要处理的子树,对于子树y,需要处理出两个数组MN,MX,MN[i]表示以x为第一个数字的不下降子序列中第i个数的最小值,MX[i]表示以x为第一个数字的不上升子序列中第i个数的最大值.如果当前子树有一个以x为首的不下降序列,那么我们就需要在之前处理的子树中找一条以x为首的满足约束条件不上升序列,可以用线段树来查询.同时每做完一颗子树的时候,用MN,MX对线段树进行更新.

区间树和线段树

注意:区间树和线段树不一样哦,线段树是一种特殊的区间树. 区间树: 区间树是在红黑树基础上进行扩展得到的支持以区间为元素的动态集合的操作,其中每个节点的关键值是区间的左端点.通过建立这种特定的结构,可是使区间的元素的查找和插入都可以在O(lgn)的时间内完成.相比于基础的红黑树数据结构,增加了一个max[x],即以x为根的子树中所有区间的断点的最大值.逻辑结构如下所示: 区间树具有和红黑树一样的性质,并且区间树的基础操作和红黑树的基础操作一样.(具体红黑树的操作,参见http://blog.cs

主席树/函数式线段树/可持久化线段树

什么是主席树 可持久化数据结构(Persistent data structure)就是利用函数式编程的思想使其支持询问历史版本.同时充分利用它们之间的共同数据来减少时间和空间消耗. 因此可持久化线段树也叫函数式线段树又叫主席树. 可持久化数据结构 在算法执行的过程中,会发现在更新一个动态集合时,需要维护其过去的版本.这样的集合称为是可持久的. 实现持久集合的一种方法时每当该集合被修改时,就将其整个的复制下来,但是这种方法会降低执行速度并占用过多的空间. 考虑一个持久集合S. 如图所示,对集合的

hdu-4819-线段树套线段树

http://acm.hdu.edu.cn/showproblem.php?pid=4819 给出一个N*N的矩阵,每次询问一个m*m的子矩阵里的floor((maxv+minv)/2)并把中间的元素修改为这个值. 线段树套线段树,第一层X表示对行建立的线段树,内层表示对Y也就是列建立的线段树. 分别对X和Y建立相应的函数来完成操作,当更新X树的节点时,再更新完当前X的节点的左右儿子之后,回头对X的这个节点对应的Y树进行更新,相当于X的左右儿子里的Y树来更新X的Y树,能理解这一点就很简单了. 1

ZJOI 2017 树状数组(线段树套线段树)

题意 http://uoj.ac/problem/291 思路 不难发现,九条カレン醬所写的树状数组,在查询区间 \([1,r]\) 的时候,其实在查询后缀 \([r,n]\) :在查询 \([l,r](l\neq1)\) 的时候,则是在查询 \([l-1,r-1]\) .那么在查询 \([1,r]\) 的时候,只需要询问 \(r\) 的前后缀异或是否相等:在查询 \([l,r](l\neq 1)\) 的时候,只需要询问 \(a[l-1],a[r]\) 是否相等. 考虑 \(O(n^2)\) 的

HDU 5649 DZY Loves Sorting(二分答案+线段树、线段树合并+线段树分割)

题意 一个 \(1\) 到 \(n\) 的全排列,\(m\) 种操作,每次将一段区间 \([l,r]\) 按升序或降序排列,求 \(m\) 次操作后的第 \(k\) 位. \(1 \leq n \leq 10^5\) 思路 两个 \(\log\) 的做法展现了二分答案的强大功能.首先二分枚举第 \(k\) 位的值,然后将小于等于它的数都变为 \(1\) ,大于它的数变为 \(0\) ,线段树可以实现对 \(01\) 序列快速的排序,按要求进行排序,然后如果第 \(k\) 位为 \(1\) 说明这