树状数组

一、使用原因

一般的数组修改操作的复杂度是O(1),求前缀和操作的复杂度是O(n);而同时维护一个前缀和数组时修改操作的复杂度是O(n),求前缀和操作复杂度是O(1);

当我们有m次操作时,时间复杂度就会达到O(mn),达到了平方级别,而我们维护一个树状数组时求前缀和和修改操作的复杂度是O(logn),总时间复杂度只有O(mlogn),达到了很快的程度。

首先要了解lowbit运算,二进制分解下最小的2的次幂。即当我们求11100的lowbit时返回的是100(二进制下)。

1 int lowbit(int x)
2 {
3     return x & -x;
4 }

二、基本思想

树状数组的本质思想是使用树结构维护”前缀和”,从而把时间复杂度降为O(logn)。

考虑一个区间[1,x],对于x,我们将其二进制展开成x=2 ^ i1 + 2 ^ i2 + 2 ^ i3 .......​+ 2 ^ ik,设i1 > i2 > i3 >.....> ik,所以我们可以把这个区间分成 log x 个小区间,长度分别为([1,2 ^ i1]),​([2 ^ i1 +1,2^ i1 + 2 ^ i2])等等,仔细观察可以发现这些小区间的共同特点都是如果区间结尾是R,那么区间长度就等于R二进制下最小的2的次幂即lowbit(R)

基于以上的理论我们可以建立一个数组tr,其中tr[x]保存序列a的区间[x-lowbit(x)+1,x]中所有数的和,我们如果知道这个数组的信息的话那么就能在O(logn)的时间内求出[1,x]的前缀和,每次x-=lowbit(x)就可以了。

对于一个序列,对其建立如下树形结构:

每个结点t[x]保存以x为根的子树中叶结点值的和
每个结点覆盖的长度为lowbit(x)
t[x]结点的父结点为t[x + lowbit(x)]
树的深度为log2n+1

两个基础操作:

1.查询(即求前缀和)

sum(x)表示将查询序列前x个数的和

以sum(7)为例:
查询这个点的前缀和,需要从这个点向左上找到上一个结点,将加上其结点的值。向左上找到上一个结点,只需要将下标 x -= lowbit(x),例如 7 - lowbit(7) = 6。

1 int sum(int x)
2 {
3     int res = 0;
4     for (int i = x; i ; i -= lowbit(i)) res += t[i];
5     return res;
6 }

2.修改

add(x, k)表示将序列中第x个数加上k。

以add(3, 5)为例:
在整棵树上维护这个值,需要一层一层向上找到父结点,并将这些结点上的t[x]值都加上k,这样保证计算区间和时的结果正确。时间复杂度为O(logn)。

1 void add(int x, int k)
2 {
3     for (int i = x; i <= n; i += lowbit(i)) t[i] += k;
4 }

 三、初始化

1.暴力初始化

直接对每一个点的都进行修改操作,时间复杂度为O(nlogn)。

1 for (int i = 1; i <= n; i ++ ) add(i, a[i]);

2.线性初始化

 考虑到我们的树状数组的本质tr[i]存储的

1 for (int i = 1; i <= n; i ++ )
2 {
3     per[i] = per[i - 1] + a[i];
4     tr[i] = per[i] - per[i - lowbit(i)];
5 }

 

是[x - lowbit(i) + 1, x]之间所有数前缀和,那么我们可以维护一个普通的前缀和数组,并且更新tr。

 

posted @ 2020-12-26 22:13  筱翼深凉  阅读(190)  评论(0)    收藏  举报