『数据结构总结1:树状数组』

『数据结构总结1:树状数组』

Preface

数据结构的总结可能就会以这种形式分好几篇发了,吸取了之前的教训,防止编辑器变卡.

如果后继总结更新了,链接会补在这里.

原理

树状数组是一种用于维护序列前缀和的数据结构,对于给定序列\(\{a_i\}\),建立序列\(\{c_i\}\)定义如下:

\[c_n=\sum_{i=n-\mathrm{lowbit}(n)+1}^n a_i \]

对于一个任意的正整数\(n\),我们都可以根据其二进制上为\(1\)的位,利用\(\mathrm{lowbit}\)运算,将\([1,n]\)划分为不超过\(\log_2 n\)个区间. 那么前缀和就可以用这\(\log_2n\)个区间和的和表示,根据序列\(\{c_i\}\)的定义,我们可以轻松地在\(\mathcal{O}(\log_2n)\)的时间内求一个序列的前缀和.

对于单点修改,我们只需考察那些\(c_x\)会被当前修改的值所影响. 我们可以将\(\{c_i\}\)看做一个树形结构(森林),每个点\(c_i\)占据区间\([i-\mathrm{lowbit}(i)+1,i]\),同时向其内部所有最大的,可以表示其区间和的\(c_j\)连边. 那么显然一个点的修改会对树上所有祖先有影响,同时不难发现一个性质节点\(c_i\)的父亲为\(c_{i+\mathrm{lowbit(i)}}\),那么修改操作就容易实现了. 根据\(\mathrm{lowbit}\)函数的性质,其时间复杂度显然也是\(\mathcal{O}(\log_2 n)\).

BinaryIndexedTree.png

实现

struct BinaryIndexedTree
{
    int c[N],n;
    inline int lowbit(int x) { return x & (-x); }
    inline void Modify(int p,int v) { for (; p <= n; p += lowbit(p)) c[p] += v ); }
    inline int Query(int p) { int s = 0; for (; p; p -= lowbit(p)) s += c[p]; return s; }
    inline int Query(int l,int r) { return Query(r) - Query(l-1); }
}

不必过多解释,唯一需要注意的地方是\(\mathrm{lowbit}(0)=0\),所以单点修改时如果涉及到下标\(0\),需要进行整体平移,否则会死循环.

功能

单点修改,区间查询

基本操作,时间复杂度均为\(\mathcal{O}(\log_2n)\).

区间修改,单点查询

不妨对原序列进行差分,那么在原序列上的操作\((l,r,+x)\)对差分序列\(\{d_n\}\)的影响为\(l\)位置\(+x\)\(r+1\)位置\(-x\). 单点查询在差分序列上的体现为前缀和.

区间修改,区间查询

同样求其差分序列\(\{d_n\}\),修改操作同上执行. 我们只需考虑如何求和:

\[\sum_{i=1}^p a_i=\sum_{i=1}^{p}\sum_{j=1}^id_j=\sum_{i=1}^{p}d_i\left(p-i+1\right)=(p+1)\times \sum_{i=1}^p d_i-\sum_{i=1}^pd_i\times i \]

维护\(\{d_i\}\)\(\{d_i\times i\}\)两个前缀和即可.

后缀操作

将修改函数和求和函数的遍历顺序反过来即可.

区间修改,区间最值

类似于线段树,从\(\mathcal{O}(\log_2n)\)个儿子处获取信息,并下传懒标记,时间复杂度\(\mathcal{O}(\log^2 n)\),无实际意义.

当操作只有单点修改,前缀最值\(/\)后缀最值查询时,可以直接按照原来的方式处理,时间复杂度\(\mathcal{O}(\log_2n)\).

维护二维平面

定义

\[c_{n,m}=\sum_{i=n-\mathrm{lowbit(n)}+1}^n\sum_{j=m-\mathrm{lowbit(m)+1}}^ma_{i,j} \]

其他概念类似,可以用完全一样的方式实现单点修改,矩阵求和,时间复杂度\(\mathcal{O}(\log^2 n)\),空间复杂度\(\mathcal{O}(n^2)\).

定义二维差分\(d_{i,j}=a_{i,j}-a_{i-1,j}-a_{i,j-1}+a_{i-1,j-1}\),显然满足\(a_{n,m}=\sum_{i=1}^n\sum_{j=1}^m d_{i,j}\). 现对原矩阵差分,则矩阵修改\((x_1,x_2,y_1,y_2,+p)\)在差分矩阵上的影响为\((x_1,y_1,+p),(x_1,y_2+1,-p),(x_2+1,y_1,-p),(x_2+1,y_2+1,+p)\). 那么容易实现矩阵修改,单点查询.

现在我们用差分实现矩阵修改,同理,考虑用差分矩阵的值表示原矩阵的一个前缀矩阵和:

\[\sum_{i=1}^n\sum_{j=1}^ma_{i,j}=\sum_{i=1}^n\sum_{j=1}^m\sum_{k=1}^i\sum_{l=1}^jd_{k,l}=\sum_{i=1}^n\sum_{j=1}^md_{i,j}\times(n-i+1)\times (m-j+1) \\ \ \\ =(nm+n+m+1)\times \sum_{i=1}^n\sum_{j=1}^md_{i,j}-(1+m)\sum_{i=1}^n\sum_{j=1}^mi\times d_{i,j}- \\ \ \\ (1+n)\sum_{i=1}^n\sum_{j=1}^mj\times d_{i,j}+\sum_{i=1}^n\sum_{j=1}^mi\times j\times d_{i,j}\]

维护\(\{d_{i,j}\}\)\(\{i\times d_{i,j}\}\)\(\{j\times d_{i,j}\}\)\(\{i\times j\times d_{i,j}\}\)四个矩阵和即可实现矩阵修改,矩阵查询.

值域树状数组

值域树状数组可以实现简易平衡树的功能,维护排名就是前缀和,插入删除一个数字就是单点修改,现在我们考虑求\(k\)大值.

考虑倍增,每次我们尝试向前\(2^i\)走步,\(i\)从大到小枚举. 现在假设答案为\(p\),那么我们想知道区间\([k,k+2^i]\)里有几个数. 如果有\(i<\mathrm{lowbit}(k)\),那么根据定义\(c_{k+2^i}\)存储的就是我们要的值. 好在倍增算法从大到小枚举\(i\),所以\(k\)累加的过程也是从高位到低位的,恰好符合我们的需求. 那么倍增求第\(k\)大,时间复杂度\(\mathcal{O}(\log_2 n)\).

inline int Select(int k)
{
    int res = 0 , cnt = 0;
    for (int i = 30; res += (1<<i) , i >= 0; i--)
        ( res > lim || cnt + c[res] >= k ) ? res -= 1<<i : cnt += c[res];
    return ++res; // if the size of BIT less than k , return size + 1 
}

例题

[LOJ2319] 列队

[CF650D] Zip-line

[BZOJ1878] HH的项链

[BZOJ1452] Count

[BZOJ3132] 上帝造题的七分钟

Epilogue

posted @ 2020-06-08 21:02  Parsnip  阅读(288)  评论(3编辑  收藏  举报