树状数组从零到一些拓展的学习笔记及模板

树状数组

为了表述方便,下面所有的数字,都是二进制形式下的。

拆分成特殊区间------C[i]的定义

树状数组通过特定将区间[1,i]\left[1,i \right]通过一个特殊地规则,将区间拆分成k(klog2i+1)k(k \leq \log_{2}{i+1})个区间(ik1,ik],(ik2,ik1],,(0,i1]]\left(i_{k-1},i_{k} \right], \left(i_{k-2},i_{k-1} \right],\dots,\left(0,i_1\right]],其中ik=ii_k=i.

而所谓特殊地规则,是这样的,首先将(l,r]\left(l,r\right],首先将rr用二进制表示,然后将最后一个1变成0即获得了左端点ll.
之后把ll作为下一个区间的右端点,重复进行,直到整个区间分完了。

举个例子。假设r=100011001001r=100011001001.划分的区间如下

l r
100011001000 100011001001
100011000000 100011001000
100010000000 100011000000
100000000000 100010000000
000000000000 100000000000

我们将数ii二进制表示形式下最后一个1的位权(即那个1及其后面的一连串的0表示的数)定义为lowbit(i)lowbit(i).那么上面拆分成的特殊区间(l,r]\left(l,r\right]拆分规则就可以用l=rlowbit(r)=rlowbit(r)l=r-lowbit(r)=r^lowbit(r)表示了。

由位运算的知识不难推导出lowbit(x)=x&(x)lowbit(x)=x\&(-x).

可以看到,区间的右端点每次变化的时候,都是去掉一个1.因此,拆分成的子区间数就是i二进制表示下1的个数。假设ii含有kk个1,而kk个1表示的最小数是11111\ldots1,即2k12^k-1,故i2k1i \geq 2^k-1,因此klog2i+1k \leq log_{2}{i+1}.

而树状数组的C[i]C[i]的定义就是[1..i][1..i]区间拆分出来的第一个区间的部分和。

假设所有的C[i]都已知,那么,对于a[1..i]a[1..i]求前缀和,我们只需要对O(log2n)O(log_{2}{n})C[j]C[j]进行求和即可。单次前缀和查询复杂度O(log2n)O(log_{2}{n}).

C[i]的求取

现在有个问题,如何求C[i]C[i]?显然直接枚举一个个元素求和是低效的、不明智的。

我们可以充分利用已经前面已经求得的C[j]C[j]的值。例如C[100011001000]C[100011001000]表示的区间是(100011000000,100011001000]\left( 100011000000, 100011001000\right].我们将前面两者相同的前缀10001100简记为A.我们可以拆分成:

l r
A0111 A1000
A0110 A0111
A0100 A0110
A0000 A0100

其中表格第一行所表示的区间只有一个元素a[A1000]a[A1000],即a[i]a[i].剩下的每一行,不难发现表示的区间恰好是C[A0111],C[A0110],C[A0100]C[A0111],C[A0110],C[A0100].表格的构造也不难,第一行只有一个元素,所以第一行的llr1r-1.之后每一行的rr都是上一行的ll,而这一行的ll就是rlowbit(r)r-lowbit(r).

事实上,也不能说是恰好。因为这是特意要这样构造的。这样拆分,可以充分利用已经求出的C[j]C[j]并且保证拆分出的C[j]C[j]不超过ii的二进制表示下0的个数。C[i]C[i]表示的区间(l,r]\left( l, r\right]必然是
(A00..0,A10..0]\left( A00..0, A10..0\right]的形式。右端点最后有kk个0,k可以取0.那么就可以将求C[i]C[i]变成求a[i]a[i]kk个已经求出来的C[j]C[j]的和的问题.显然kkO(log2n)O(log_{2}{n}).总体C数组的求取是O(nlog2n)O(nlog_{2}{n}).

为什么说是树状数组呢?去百度百科里看一下图就知道了。就是把C[i]C[i]表示的区间往求取C[i]C[i]所要用到各个C[j]C[j]连一条边,并且往a[j]a[j]连一条边,就变成一棵树了。最后就变成树一样的形式了。而C[i]C[i]的高度,显然取决于ii的二进制表示下11的个数(因为每一层都去掉最后一个1),即至多log2i+1log_{2}{i+1}。因此最高点的高度是O(log2n)O(log_{2}{n})

单点修改a[i]快速更新C数组

到现在为止,我们通过把区间分解成一个个特殊的区间,并且用一个数组C记录这一个个小区间。如果仅仅是求前缀和,那么现在这种做法没有任何优势。因为它多维护了很多不必要的部分区间和。预处理时间复杂度O(nlog2(n))O(nlog_2(n))比前缀和O(n)O(n)差,单次查询前缀和O(log2n)O(log_{2}{n})比前缀和O(1)O(1)差。

但是,我们的确维护了更多的信息,只是这些多维护的信息暂时还没有得到充分的利用,所以显得多余。

当我们就行元素a[i]a[i]的单点修改的时候,树状数组多维护的部分区间和信息就开始显示出优势了。对于朴素的数组前缀和,当我们修改一个元素a[i]a[i]只是,所有sum[j](ji)sum[j](j \geq i)都要更新。而对于树状数组而言,由于只是维护了部分区间和,所以修改a[i]a[i]之时,当且仅当C[j]C[j]所管辖的区间(jlowbit(j),j](j-lowbit(j),j]包含ii之时才需要更新C[j].

那么更新a[i]a[i]时,如何找到需要更新的所有C[j]C[j]呢?

只需要解决一个问题即可。刚才是我们求C[i]C[i]时,是直接找到了直接影响它的所有C[j]C[j]。即由父亲C[i]C[i]直接找到了所有的儿子C[j]C[j]及儿子a[i]a[i].那么如何由某一个儿子找到父亲呢?

如果是a[i]a[i],那么父亲显然是c[i]c[i];如果是c[j],观察上面的例子,不难发现是父亲C[i]C[i]中的i=j+lowbit(j)i=j+lowbit(j),而通过之前构造出c[j]c[j]的过程也不难验证这个猜想正确。

因此,更新a[i]a[i]的时候,只需要一直往上寻找父亲并更新父亲的C值,直到发现到顶了。由于每次都是加自己的lowbit往上走,顶再往上走就超过n了。

BTW,其实C[i]的预处理求取也可以直接通过更新操作来求取,a视作全0数组,那么C数组不论是哪个区间的部分和,和都是0.然后将a[1],a[2],a[3],,a[n]a[1],a[2],a[3],\ldots,a[n]逐一更新即可。复杂度是一样的。

小结

树状数组C,通过线性的空间复杂度维护一类特殊区间((ilowbit(i),i]\left( i-lowbit(i), i\right])的部分和信息,以对数的时间复杂度实现了单次自顶向下查询原数组a前缀和的操作,以对数的时间复杂度实现了单次自底向上的修改a[i]并更新C数组的操作。

Code

树状数组的代码非常简单,简直令人发指。相对于线段树简单很多,而且常数比线段树小不少。当然,线段树可能多支持一些操作。

inline int lowbit(int x) {return x&(-x);}
inline int lft(int x) {return x&(-x)^x;}
inline int upf(int x) {return x+lowbit(x);}
// 原始数组a下标从1开始
// 树状数组 c[i]是初始数组a的部分区间和
// i所掌管的区间是(lft(i),i]
ll a[maxn];
int n;
ll c[maxn];

void add(int i,ll val) {
	while (i <= n) c[i] += val, i = upf(i);
}

ll sum(int i) {
	ll ans = 0;
	while (i) ans += c[i],i = lft(i);
	return ans;
}

一些发散的思考

C[i]的更广泛的定义

前面我们将C[i]定义成了特定区间的区间部分和。但是C[i]不一定要是定义成区间的部分和,只要是满足结合律的运算的函数应该都可以,例如定义成区间内所有元素的异或值、最值之类的。

但是考虑到单点修改a[i]的时候,我们是把包含a[i]a[i]C[j]C[j]进行更新的。

假设直接指定了修改操作是"+val"(这里的+不是普通意义的加法,只是代表一种运算)。理论上更新后的C[j]C^{'}[j]的值应该是+a[i]+val+\ldots+a[i]+val+\ldots,而我们修改操作的算法是C[i]=C[i]+valC^{'}[i]=C[i]+val,即计算的是+a[i]++val\ldots+a[i]+\ldots+val.理论上的值要和我们实际计算出来的值恒相等就必须使得我们定义的+运算必须满足交换性。

如果单点修改操作不是直接给出如何修改,而是给出修改后的结果,即直接给出修改后的a[i]a^{'}[i],则定义的+运算则还需要加上存在逆元(逆操作)a-a这一条。即+a[i]+\ldots+a[i]+\ldots要变成+a[i]+\ldots+a^{'}[i]+\ldots,如果可交换并且有逆元,那么就可以+a[i]+a[i]+a[i]\ldots+a[i]+\ldots-a[i]+a^{'}[i]计算出目标值。即可以消去原本a[i]a[i]的影响。例如异或可以,但是求区间最大值却不可(因为无逆元)。

当然,如果不要求保持单次修改操作的时间复杂度是O(log2n)O(log_{2}{n}),那么只需要满足结合性即可。显然,我们需要修改我们的修改操作的算法。我们依旧是自底向上更新。对于C[j]C[j]的更新,我们不能利用原本的C[j]C[j]的信息直接计算,而是用C[j]C[j]的所有儿子按照其所代表的区间顺序(如果不满足交换性)重新计算一番。另外,对于区间a[l,r]a[l,r]的问题,不能变成a[1..r]a[1..r]a[1..l1]a[1..l-1]的差的问题,因为无法消除a[1..l1]a[1..l-1]的影响。于是只能使用逐步分解为儿子的一个个区间后进行的合并的方法。单点修改复杂度将变成O(log22n)O({log^2_{2}{n}})并且代码没那么优美了.因此,最值问题也可以使用树状数组做,只是复杂度多乘以了一个对数。

BTW,事实上,线段树之所以能处理最值问题就是因为线段树采取的是重新用儿子计算。只是,线段树的一个节点的儿子就只有两个!

与差分的结合

小结中已经可以看到,树状数组支持单点修改,前缀和查询(之后容易推出区间和)查询。

那么区间集体增加一个数,查询单点值呢?这个引入差分数组即可。区间集体加将等价于差分数组的两个点的单点修改。查询单点值将等价于查询差分数组的前缀和。

如果是区间集体加,查询区间和呢?依旧引进差分数组b,则前缀和i=1la[i]=i=1lj=1ib[i]=i=1l[b[i]×(ni+1)]\sum\limits_{i=1}^{l}{a[i]}=\sum\limits_{i=1}^{l}\sum\limits_{j=1}^{i}b[i]=\sum\limits_{i=1}^{l}\left[ b[i] \times (n-i+1) \right].所以只需要引入数组c[i]=b[i]×(ni+1)c[i]=b[i] \times (n-i+1).如此,区间集体加将是变成c数组的2次单点修改,区间求和将变成c数组的两次前缀和查询。再次变成树状数组的题目。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章