【学习记录】树状数组

能用一个二进制数分出 O ( log n ) O(\log n) 个小区间:从 [ x lowbit ( x ) + 1 , x ] [x - \operatorname{lowbit}(x) + 1, x] 递归下去。利用这种思想,对于一个数组 a [ i ] a[i] ,可以构造出一个数组 c [ i ] c[i] ,其中 c [ x ] = i = x lowbit ( x ) + 1 x a [ i ] c[x] = \sum _{i=x - \operatorname{lowbit}(x) + 1}^{x} a[i] 。树状数组就是这样一个数组。

单点修改,单点求值

树状数组的基本模型是单点修改和求和。

要修改 a [ i ] a[i] ,就要修改掉所有包含 a [ i ] a[i] c [ x ] c[x] 。根据 c [ x ] c[x] 的公式,只有所有 x i x\ge i lowbit ( x ) lowbit ( i ) \operatorname{lowbit}(x) \ge \operatorname{lowbit}(i) x x 满足条件。因此可以使用 i += lowbit(i) 不断获取下一个要更新的节点。

要查询 i = 1 r a [ i ] \sum_{i=1}^{r} a[i] ,就按照上面分区间的思想,使用 r -= lowbit(r) 不断获取下一个要累计的区间。

上面两个操作的时间复杂度均为 O ( log n ) O(\log n)

inline int lowbit(int x){
    return (-x) & x;
}
void add(int k, int v){
    while (k <= n)
        c[k] += v, k += lowbit(k);
}
int query(int r){
    int res = 0;
    while (r > 0)
        res += c[r], r -= lowbit(r);
    return res;
}

初始化

初始化一个树状数组可以使用依次单点更新的方法,这样做是 O ( n log n ) O(n\log n) 的。有两种更好的线性的初始化方式。

一种是利用 c [ x ] c[x] 管理的区间为 ( x lowbit ( x ) , x ] (x - \operatorname{lowbit}(x), x] ,先处理出前缀和然后直接做。

另一种不需要额外空间,只需要简单的递推即可。代码如下所示。

for (int i = 1; i <= n; ++i)
    a[i] = c[i];
for (int i = 2; i <= n; i <<= 1)
    for (int j = i; j <= n; j += i)
        c[j] += c[j - (i >> 1)];

区间修改,单点求值

维护原来数列的差分数列即可。

区间修改,区间求值

还是维护原来数列的差分数列。设 a [ 0 ] = 0 a[0]=0 ,则 d [ i ] = a [ i ] a [ i 1 ] , a [ i ] = j = 1 i d [ j ] d[i] = a[i] - a[i - 1], a[i]=\sum_{j = 1}^{i}d[j] 。因此有
i = 1 r a [ i ] = i = 1 r j = 1 i d [ j ] = i = 1 r ( r i + 1 ) d [ i ] = ( r + 1 ) i = 1 r d [ i ] i = 1 r i × d [ i ] \sum_{i = 1}^{r} a[i] = \sum_{i = 1}^{r}\sum_{j = 1}^{i}d[j] = \sum_{i = 1}^{r} (r-i + 1)d[i] = (r+1)\sum_{i = 1}^{r} d[i] - \sum_{i = 1}^{r} i\times d[i]

所以另外维护一个 i × d [ i ] i\times d[i] 的树状数组即可。

int c1[100005], c2[100005], n;
void add(int r, int k){
    for (int i = r; i <= n; i += lowbit(i))
        c1[i] += k, c2[i] += k * r;
}
ll query(int r){
    ll res = 0;
    for (int i = r; i > 0; i -= lowbit(i))
        res += 1ll * (r + 1) * c1[i] - c2[i];
    return res;
}

求第 k k 小值

仿照权值线段树的思想,我们构建权值树状数组,然后在权值树状数组上倍增。下面的 a [ i ] a[i] 表示原序列被离散化成 i i 的数有多少个。

我们现在要找第 k k 小的值,也就是找到最小的下标 x x ,满足 i = 1 x a [ i ] k \sum_{i=1}^{x} a[i] \ge k 。这可以转化成找到最大的下标 x x 使得 i = 1 x a [ i ] < k \sum_{i=1}^{x} a[i] < k ,那么结果就是 x + 1 x+1 。这一点和倍增很像。

而后的过程和倍增更像:我们从大到小枚举 bit,利用树状数组的性质检查上面那一点,如果成立就加入这个 bit。时间复杂度 O ( log n ) O(\log n)

int kth(int k){
    int cnt = 0, ret = 0;
    for (int i = 18; i >= 0; --i)
        if (ret + (1 << i) < n && cnt + c[ret + (1 << i)] < k)
            ret += (1 << i), cnt += c[ret];
    return ret + 1;
}

典型例题如 POJ 2182。

树状数组与时间戳

在 CDQ 分治等算法中,树状数组需要被频繁地更新和重置。这带来的时间成本是很高的。

因此可以对树状数组的每一个下标维护一个时间戳,如果在更新某一个下标时发现时间戳不是正在使用的,那么就重置该下标的值;如果是在询问时发现,那就不计入该下标的答案。

int D, tag[100005];
inline int lowbit(int x){
    return (-x) & x;
}
void add(int k, int v){
    while (k <= n){
        if (tag[k] != D) tag[k] = D, c[k] = 0;
        c[k] += v, k += lowbit(k);
    }
}
int query(int r){
    int res = 0;
    while (r > 0){
        if (tag[r] == D) res += c[r];
        r -= lowbit(r);
    }
    return res;
}

二维树状数组

树状数组可以很方便地放到二维上。修改的时候两个维度都跑一次加法即可,前缀和查询类似。时间复杂度均为 O ( log 2 n ) O(\log^2 n)

其他

树状数组好像还可以用来处理区间最值问题,但是貌似没有线段树方便好写,所以就没有看了。

但是树状数组还是可以比较方便的用来处理前缀最值相关的问题的。典型例题如 SCOI2014 方伯伯的玉米田。

参考资料

猜你喜欢

转载自blog.csdn.net/zqy1018/article/details/108294824