树状数组(一) 基本结构及其基本运用

引入

首先我们来看一张表:

单点修改 区间修改
单点查询 传统数组
区间查询

当我们使用普通的数组时,如果我们要对一个元素进行修改,时间复杂度为 \(O(1)\) ,如果我们要进行区间查询,时间复杂度则为 \(O(n)\) ,我们知道,一个算法的时间复杂度是由瓶颈(时间复杂度最高的那一项)决定的,所以我们要实现一种较为快速的数据结构,他的单点修改为 \(O(log \ n)\) ,区间查询为 \(O(log \ n)\) 。这样,就引出了今天的主题——树状数组

原理

在树状数组中,顾名思义,我们要以数组的形式建树,具体怎么建呢?请看下面这个例子:

image

其中蓝色部分表示原始数组(A),黄色部分表示树状数组(C),红线表示每个节点所管理的元素。
发现:
C[1] = A[1];
C[2] = A[1] + A[2];
C[3] = A[3];
C[4] = A[1] + A[2] + A[3] + A[4];
C[5] = A[5];
C[6] = A[5] + A[6];
C[7] = A[7];
C[8] = A[1] + A[2] + A[3] + A[4] + A[5] + A[6] + A[7] + A[8];

转换成二进制得到:
C[1] = A[1];
C[10] = A[1] + A[10];
C[11] = A[11];
C[100] = A[1] + A[10] + A[11] + A[100];
C[101] = A[101];
C[110] = A[101] + A[110];
C[111] = A[111];
C[1000] = A[1] + A[10] + A[11] + A[100] + A[101] + A[110] + A[111] + A[1000];

发现:
\(C[x] = \sum _{i =x - lowbit(x) + 1} ^ x A[i]\)
\(C[x]\) 维护的是 \((x - lowbit(x), x]\) 这个区间(注意左边是闭区间
\(lowbit(x)\) 表示的是 x 在二进制下最右边的1 (例如 \(lowbit(10010_2)=10_2\) )。
至于为什么这么设计,是因为他有着非常神奇的性质,我们后面会讲到。

lowbit

在讲实现之前,我们先了解一下这个所谓的 "lowbit" 怎么来用代码写出。如果从左往右一位一位地算的话,复杂度为 \(O(log\ n)\) ,有没有更快速的方法呢?答案肯定是有的:
inline int lowbit(int &x) { return x & -x; }
感性理解一下,负数是由补码存储的,-x 等于 x的取反加一,那么原先x末尾的1000...0,在-x中就会变为0111...1(取反)-> 1000...0(加一),而剩余的高位刚好相反,下面是三个例子:

x x取反 x取反加一(-x) x & -x
10100010 01011101 01011110 00000010
11111111 00000000 00000001 00000001
11101000 00010111 00011000 00001000

了解了 lowbit 以及基本的原理之后,我们就可以开始写基本的操作了。

单点修改

当我们进行单点修改的时候,我们需要找到的就是 包含原始数组A[i]的所有树状数组C[j],并进行修改操作,具体什么意思呢,举个例子。
当我们修改5号位置时,绿色为要更新的节点:

image

从上图可以发现,当我们更新A[5]的时候,C[5]、C[6]、C[8]都被更新了。
观察他们的二进制: \(5 (101)_2 \ \ \ \ \ 6(110)_2 \ \ \ \ 8(1000) _2\) ,可以得知
$5 + lowbit(5) = (101)_2 + (1)_2 = (110)_2 = 6 $
$6 + lowbit(6) = (110)_2 + (10)_2 = (1000)_2 = 8 (x - lowbit(x), x] $
.......(后面会一直更新到整棵树的大小)

于是可以发现当更新A[i]时,会更新
$ C[i],C[i_1],C[i_2]...C[i_n] $
其中 $i_k = i_{k-1} + lowbit(i_k), i_k \leq n $

理解一下这个结论:有哪些节点会包含A[i]这个元素呢?首先C[i]肯定是包含的(原因上面已经讲过),再看看有没有其他的节点 \(x\) 使得 \(i \in (x - lowbit(x), x]\) ,很容易得出这样一个结论:如果C[x]包含A[i],那么C[x + lowbit(x)]也一定包含A[i]
我们来证明这个结论:我们设原来的 x 为 x1,x + lowbit(x) 为 x2。
那么 x1 所代表的区间为 \((x_1 - lowbit(x_1), x_1]\)
x2所代表的区间为 \((x_2 - lowbit(x_2), x_2]\)
\(\because x_1 < x_2\)
$\therefore x_1 + lowbit(x1) < x_2 + lowbit(x_2) $
也就是说** x2 所代表的区间完全包含 x1 所代表的区间** 。那么更新 C[x1] 时,就要更新 C[x2] 了。

于是代码就自然而然地写出来了(将位置为 x 的元素加上 v ,这里用 f 表示树状数组 C )
inline void updata(int x, int v) { while(x <= n) { f[x] += v; x += lowbit(x); } }

区间查询

假设我们现在要查询区间 \([l, r]\) ,那么就将问题转化成查询区间 \([1, l)\)\([1, r]\) ,通过第二项减去第一项就可以得到区间 \([l, r]\) 了。
我们还是从特殊到一般:假设现在查询区间 \([1, 7]\) 。其中紫色部分为需要累加的和.

image

与之前相似地,观察他们的二进制 \(7(111) \ \ \ 6(110) \ \ \ 4(100)\) ,易得
$7 - lowbit(7) = (111)_2 - (1)_2 = (110)_2 = 6 $
$6 - lowbit(6) = (110)_2 - (10)_2 = (100)_2 = 4 $
$4 - lowbit(4) = (100)_2 - (100)_2 = (0)_2 = 0 $
总结后可以发现紫色节点有以下规律( \(sum_x\) 表示区间 \([1, x]\) 的和)
$sum_x = C[x] + C[x_1] + C[x_2]+..+C[x_n] $
其中 \(x_k = x_{k - 1} - lowbit(x_{k - 1})\)

首先 \(sum_x\) 中一定包含 C[x], 而C[x]所代表的范围是 \((x - lowbit(x), x]\) ,之后我们要找到一个节点 y ,他所代表的区间为: \((y - lowbit(y), y]\) , 而我们要完整包含 \([1, x]\) 这整个区间, 所以 y 一定要满足: \(y =x - lowbit(x)\) (x的左界等于y的右界),于是得证。

代码就比较好写了:

inline int pre(int x) { int res = 0; while(x) { res += f[x]; x -= lowbit(x); } return res; }
inline int query(int l, int r) { return pre(r) - pre(l - 1); }

例题

树状数组1[模板]-洛谷
这里维护的操作,之前我们都讲过了,这里只把代码放出来

#include <bits/stdc++.h>
using namespace std;

inline int read() {//快读,可忽略
    int x = 0; char c = getchar();bool f = 1;
    while(c < '0' || c > '9') { if(c == '-') f = 0; c = getchar(); }
    while(c >= '0' && c <= '9') { x = x * 10 + (c - '0'); c = getchar(); }
    return (f ? x : -x);
}

int n, m, opt, x, y, f[500005];

inline int lowbit(int &x) { return x & -x; }
inline void updata(int x, int v) { while(x <= n) { f[x] += v; x += lowbit(x); } }
inline int pre(int x) { int res = 0; while(x) { res += f[x]; x -= lowbit(x); } return res; }
inline int query(int l, int r) { return pre(r) - pre(l - 1); }

int main() {
    n = read(); m = read();
    for(int i = 1 ;i <= n ;++i) updata(i, read());

    while(m--) {
        opt = read(); x = read(); y = read();

        if(opt == 1) updata(x, y);
        else printf("%d\n", query(x, y));
    }

    return 0;
}

尾声

让我们回到开始的那张表:

单点修改 区间修改
单点查询 传统数组 ?
区间查询 树状数组 ?

现在我们已经掌握了 单点修改 + 单点查询 以及 单点修改 + 区间查询,那么后两种 区间修改 + 单点查询区间修改 + 区间查询 暂时还没有掌握,这就是我们下几章要讲述的内容了。

posted @ 2022-04-11 11:57  NeoNie  阅读(8)  评论(0)    收藏  举报