【学习记录】树状数组

能用一个二进制数分出 O ( log ⁡ n ) O(\log n) O(logn) 个小区间:从 [ x − lowbit ⁡ ( x ) + 1 , x ] [x - \operatorname{lowbit}(x) + 1, x] [xlowbit(x)+1,x] 递归下去。利用这种思想,对于一个数组 a [ i ] a[i] a[i],可以构造出一个数组 c [ 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] c[x]=i=xlowbit(x)+1xa[i]。树状数组就是这样一个数组。

单点修改,单点求值

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

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

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

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

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) O(nlogn) 的。有两种更好的线性的初始化方式。

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

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

for (int i = 1; i <= n; ++i)
    c[i] = a[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 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] d[i]=a[i]a[i1],a[i]=j=1id[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=1ra[i]=i=1rj=1id[j]=i=1r(ri+1)d[i]=(r+1)i=1rd[i]i=1ri×d[i]

所以另外维护一个 i × d [ i ] i\times d[i] i×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 k 小值

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

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

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

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) O(log2n)

其他

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

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

参考资料

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值