sss

『数据结构总结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_ileft(p-i+1 ight)=(p+1) imes sum_{i=1}^p d_i-sum_{i=1}^pd_i imes i ]

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

后缀操作

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

区间修改,区间最值

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

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

维护二维平面

定义

[c_{n,m}=sum_{i=n-mathrm{lowbit(n)}+1}^nsum_{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}^nsum_{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}^nsum_{j=1}^ma_{i,j}=sum_{i=1}^nsum_{j=1}^msum_{k=1}^isum_{l=1}^jd_{k,l}=sum_{i=1}^nsum_{j=1}^md_{i,j} imes(n-i+1) imes (m-j+1) \ \ =(nm+n+m+1) imes sum_{i=1}^nsum_{j=1}^md_{i,j}-(1+m)sum_{i=1}^nsum_{j=1}^mi imes d_{i,j}- \ \ (1+n)sum_{i=1}^nsum_{j=1}^mj imes d_{i,j}+sum_{i=1}^nsum_{j=1}^mi imes j imes d_{i,j}]

维护({d_{i,j}})({i imes d_{i,j}})({j imes d_{i,j}})({i imes j imes 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

原文地址:https://www.cnblogs.com/Parsnip/p/13068487.html