树状数组

Posted whx1003

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树状数组相关的知识,希望对你有一定的参考价值。

题目 树状数组模板1 题目大意:给定一个序列,要求支持两种操作:1.将某个数加上 (x),2.查询区间某部分的和。

题目 树状数组模板2 题目大意:给定一个序列,要求支持两种操作:1.将区间内的每个数加上 (x),2.查询某个数。

分析 由于第一题与第二题实际上是等价的(稍后会说明),我们以下仅讨论第一题。我们先以最简单的数组来考虑,显然操作1是 (O(1)),操作2是 (O(n)) 的,总体复杂度 (O(nm)),很明显会超时。这时我们就会想到用前缀和来优化操作2,这样的话操作2是 (O(1)) 的,但是操作1是 (O(n)) 的,总体复杂度仍为 (O(nm)),会超时。有没有一种数据结构,可以使操作1和操作2都是 (O(1)) 的呢?仅目前来看是不存在的。于是我们退而求其次寻找一种数据结构,使操作1和操作2都是 (O(log n)) 的。

树状数组(又称二叉索引树,Fenwick树)是一类最基础的树形数据结构。它支持 (O(log n)) 进行区间修改单点查询或区间查询单点修改,亦即上述两道题的操作。具体来说,树状数组自身本质上仍是一个数组。我们令 (a) 为原数组,(c) 为树状数组,假定 (i) 二进制最后有 (j) 个0,则有 (c_i=sum_{k=0}^{2^j}a_{i-k})

技术图片
(图片来自网络,侵删)

不难发现,现在我们更改与查询的时候只需更改包含着对象的位置就可以了,这样的位置不会超过 (log n) 个(为什么),所以我们只需 (O(1)) 从一个位置找到包含原对象的另一个位置就行了。

如果当前要修改 (1(0001)),则要修改 (1(0001),2(0010),4(0100),8(1000))

如果当前要修改 (5(0101)),则要修改 (5(0101),6(0110),8(1000))

如果当前要查询 (7(0111)) 的前缀和,则要查询 (7(0111),6(0110),4(0100))

如果当前要查询 (5(0101)) 的前缀和,则要查询 (5(0101),4(0100))

不难发现,修改一个数时,下一个要修改的目标位置就是当前位置加上当前位置的最后一位,如 (6 ightarrow 8(0110+0010=1000));查询一个数时,下一个要修改的目标位置就是当前位置减去当前位置的最后一位,如 (5 ightarrow 4(0101-0001=0100))。那么现在问题转化为了如何 (O(1)) 求出某个数二进制最后一位,那便是lowbit函数:

int lowbit(int x)
{
    return x & (-x);
}

这个函数可以以 (O(1)) 的优秀复杂度求出某个数二进制的最后一位(比如 (3(0011) ightarrow 1(0001),6(0110) ightarrow 2(0010))),证明只需一点点基础的二进制编码,留给读者自证。

那么现在我们就可以愉快地写出第一道题的代码。而对于第二题,不难发现,只要对原数列进行差分,就可以将区间修改转变为单点修改,单点查询转变为区间查询。

代码
第一题:

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

const int maxn = 5E+5 + 5;

int n, m;
int c[maxn];

int Read()
{
    int x = 0, op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9') {
        if(ch == '-') op = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch - '0');
        ch = getchar();
    }
    return x * op;
}

inline int lowbit(int x) { return x & (-x); }
inline void Add(int pos, int x) { while(pos <= n) c[pos] += x, pos += lowbit(pos); }

int Query(int pos)
{
    int res = 0;
    while(pos) {
        res += c[pos];
        pos -= lowbit(pos);
    }
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i) Add(i, Read());

    while(m--) {
        int op = Read(), x = Read(), y = Read();

        if(op == 1) Add(x,y);
        else printf("%d
", Query(y) - Query(x - 1));
    }
}

第二题:

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

const int maxn = 5E+5 + 5;

int n, m;
int a[maxn], c[maxn];

int Read()
{
    int x = 0, op = 1;
    char ch = getchar();
    while(ch < '0' || ch > '9') {
        if(ch == '-') op = -1;
        ch = getchar();
    }
    while(ch >= '0' && ch <= '9') {
        x = (x << 3) + (x << 1) + (ch - '0');
        ch = getchar();
    }
    return x * op;
}

inline int lowbit(int x) { return x & (-x); }
inline void Add(int pos, int x) { while(pos <= n) c[pos] += x, pos += lowbit(pos); }

int Query(int pos)
{
    int res = 0;
    while(pos) {
        res += c[pos];
        pos -= lowbit(pos);
    }
    return res;
}

int main()
{
    scanf("%d%d", &n, &m);
    for(int i = 1; i <= n; ++i) a[i] = Read();

    while(m--) {
        int op = Read(), x = Read(), y, z;

        if(op == 1) y = Read(), z = Read(), Add(x, z), Add(y + 1, -z);
        else printf("%d
", a[x] + Query(x));
    }
}

以上是关于树状数组的主要内容,如果未能解决你的问题,请参考以下文章

数据结构之树状数组从零认识树状数组

树状数组和线段树有啥区别?

树状数组

树状数组

树状数组

如何利用树状数组修改一个区间?