树状数组

前言:树状数组仅可以解决线段树问题的子集:单点修改 + 区间查询。‘

它利用的是区间的可差分性(可减性)以及位运算相关的知识构建一颗类似压缩版的线段树。

它相较于线段树的优点

  • 时间复杂度为严格的 O(logn),具有较小的常数;空间复杂度为 O(n)。
  • 实现起来简单,往往仅需十几行的代码。

相较于线段树的缺点

  • 解决的问题场景受限,仅可以解决单点修改 + 区间查询操作。
  • 树状数组的场景一般都是利用的可差分性,比较难想。

树状数组维护信息的方式及性质

问题:对于一个数组 a,我们需要快速求出数组内[l, r]的区间和,以及可以完成对应的修改操作。

之前用线段树解决这道问题的时候,我们会先创建一颗线段树,这里以 1 - 8 为例。

这个时候我们发现:有些结点其实并没有维护的必要,比如,[2, 2] 结点的信息可以由 [1, 2] 和 [1, 1] 的信息相减得到(可差分性),这样我们把多余的节点信息删除。

把剩余结点的信息存到一个一位数组中,就可以得到树状数组。实际上树状数组的由来不是线段树删减结点得到的,它是由位运算的知识推导出来的,知识这样便于理解,想要由线段树删减结点的来树状数组需要数组长度为 2

的幂次。

树状数组的性质

  • 向上爬公式:x <- x + lowbit(x)。
  • 往前跳公式:x <- x - lowbit(x)。
  • 维护区间信息:x -> [x - lowbit(x) + 1, x]。

一维树状数组

对于一个长为 n 的序列,完成下列操作。

  • 单点修改。
  • 查询区间和。

树状数组的修改操作

修改完单点之后,需要从该点一直爬到根节点,每个结点上都要加上修改的那个数 k。

void modify(int x, LL k) 
{
    for(int i = x; i <= n; i += lowbit(i)) s[i] += k;
}

树状数组的创建

它的创建就是不断插入新节点的过程,不断掉用modify操作。

cin >> n >> q;
    for(int i = 1; i <= n; i++) 
    {
        cin >> a[i];
        modify(i, a[i]);
    }

树状数组的查询

查询操作是利用数组的可减性,利用前缀和来实现。

LL query(int x)
{
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i)) sum += s[i];
    return sum;
}

树状数组 1 :单点修改,区间查询

#include <iostream>

using namespace std;
#define lowbit(x) (x & -x)
typedef long long LL;
const int N = 1e6 + 10;
int a[N], n, q;
LL s[N]; // 树状数组

void modify(int x, LL k)
{
    for(int i = x; i <= n; i += lowbit(i)) s[i] += k;
}

LL query(int x)
{
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i)) sum += s[i];
    return sum;
}
int main()
{
    cin >> n >> q;
    for(int i = 1; i <= n; i++) 
    {
        int x; cin >> x;
        modify(i, x);
    }
    while(q--)
    {
        int op; cin >> op;
        int l, r; cin >> l >> r;

        if(op == 1) modify(l, r);
        else cout << query(r) - query(l - 1) << endl;
    }
    return 0;
}

树状数组 2 :区间修改,单点查询

树状数组只能解决 单点修改 + 区间查询的操作。 针对上面问题,维护原数组的差分数组。

#include <iostream>

using namespace std;
#define lowbit(x) (x & -x)
typedef long long LL;
const int N = 1e6 + 10;
LL s[N];
int n, q, a[N];
void modify(int x, LL k)
{
    for(int i = x; i <= n; i += lowbit(i)) s[i] += k;
}
LL query(int x)
{
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i)) sum += s[i];
    return sum;
}
int main()
{
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
    {
        cin >> a[i];
        modify(i, a[i] - a[i - 1]);
    }
    while(q--)
    {
        int op; cin >> op;
        if(op == 1)
        {
            int l, r, x; cin >> l >> r >> x;
            modify(l, x); modify(r + 1, -x);
        }
        else 
        {
            int i; cin >> i;
            cout << query(i) << endl;
        }
    }
    return 0;
}

树状数组 3 :区间修改,区间查询

针对于区间修改,我们还是维护原数组的差分数组。

难处理的是区间查询,查询区间 [l, r] 的和,还是采用 [1, r] - [1, l - 1] 处理。

但是这样时间复杂度退化到了 n,为了简化这一部分的时间复杂度我们对其化简:

这样我们可以维护两个树状数组来解决问题。但是一般对于区间查询 + 区间修改的操作能用线段树则用线段树。

#include <iostream>

using namespace std;

typedef long long LL;
#define lowbit(x) (x & -x)
const int N = 1e6 + 10;
int n, q;

class BIT
{
    LL s[N];
public:
    void modify(int x, LL k)
    {
        for(int i = x; i <= n; i += lowbit(i)) s[i] += k;
    }
    LL query(int x)
    {
        LL ret = 0;
        for(int i = x; i; i -= lowbit(i)) ret += s[i];
        return ret;
    }
}A, B; // d d * (i - 1)

int main()
{
    cin >> n >> q;
    for(int i = 1; i <= n; i++)
    {
        LL x; cin >> x;
        A.modify(i, x); A.modify(i + 1, -x);
        B.modify(i, x * (i - 1)); B.modify(i + 1, -x * i);
    }
    int op, l, r;
    LL x;
    while(q--)
    {
        cin >> op;
        if(op == 1)
        {
            cin >> l >> r >> x;
            A.modify(l, x); A.modify(r + 1, -x);
            B.modify(l, x * (l - 1)); B.modify(r + 1, -x * r);
        }
        else
        {
            cin >> l >> r;
            LL ret1 = A.query(r) * r - B.query(r);
            LL ret2 = A.query(l - 1) * (l - 1) - B.query(l - 1);
            cout << ret1 - ret2 << endl;
        }
    }
    return 0;
}

二维树状数组

二维树状数组的创建与修改操作可以类比一维树状数组。

创建和修改操作:x <- x + lowbit(x) ,实际上是把包含 x 的区间全部修改,在二维里面 修改(i,j) 位置,就在 i,j位置上分别网上爬。

查询操作:查询 (x1, y1) - (x2, y2) 的区间和,利用前缀和得:query(x2, y2) - query(x1 - 1, y2) - query(x2, y1 - 1) + query(x1 - 1, y1 - 1)。在二维里面分别往前跳就可以。

二维树状数组 1:单点修改,区间查询

#include <iostream>

using namespace std;
#define lowbit(x) (x & -x)
typedef long long LL;
const int N = 5e3 + 10;

LL s[N][N];
int n, m;

void modify(int x, int y, LL k)
{
    for(int i = x; i <= n; i += lowbit(i))
    {
        for(int j = y; j <= m; j += lowbit(j))
        {
            s[i][j] += k;
        }
    }
}

LL query(int x, int y)
{
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i))
    {
        for(int j = y; j; j -= lowbit(j))
        {
            sum += s[i][j];
        }
    }
    return sum;
}

int main()
{
    cin >> n >> m; 
    int op; 
    while(cin >> op)
    {
        if(op == 1)
        {
            int x, y; LL k;
            cin >> x >> y >> k;
            modify(x, y, k);
        }
        else
        {
            int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
            cout << query(x2, y2) - query(x1 - 1, y2) - query(x2, y1 - 1) + query(x1 - 1, y1 - 1) << endl;
        }
    }

    return 0;
}

二维树状数组 2:区间修改,单点查询

类比一维树状数组,利用差分解决。

#include <iostream>

using namespace std;
typedef long long LL;
#define lowbit(x) (x & -x)
const int N = 5000;
LL s[N][N];
int n, m, q;

void modify(int x, int y, LL k)
{
    for(int i = x; i <= n; i += lowbit(i))
    {
        for(int j = y; j <= m; j += lowbit(j))
        {
            s[i][j] += k;
        }
    }
}

LL query(int x, int y)
{
    LL sum = 0;
    for(int i = x; i; i -= lowbit(i))
    {
        for(int j = y; j; j -= lowbit(j))
        {
            sum += s[i][j];
        }
    }
    return sum;
}

int main()
{
    cin >> n >> m;
    int op; 
    while(cin >> op)
    {
        if(op == 1)
        {
            int x1, y1, x2, y2; cin >> x1 >> y1 >> x2 >> y2;
            LL k; cin >> k;
            modify(x1, y1, k);
            modify(x2 + 1, y1, -k);
            modify(x1, y2 + 1, -k);
            modify(x2 + 1, y2 + 1, k);
        }
        else
        {
            int x, y; cin >> x >> y;
            cout << query(x, y) << endl;
        }
    }
    return 0;
}

二维树状数组 3:区间修改,区间查询

还是那句话,针对于区间修改和区间查询的操作,能用线段树解决就用线段树解决。

我们还是类比一维树状数组的区间修改 + 区间查询操作。
对于区间查询 (x1, y1) - (x2, y2) 子矩阵的和,我们计算每个差分位置被计算了多少次

\(sum = \sum_{i = 1}^{x2}\sum_{j=1}^{y2} d[i][j] * (x2 - i + 1) * (y2 - j + 1). \)

->\(sum = \sum_{i = 1}^{x2}\sum_{j=1}^{y2} d[i][j] * ((x2*y2+1+x2+y2)- i(y2 + 1) - j(x2 + 1) + ij) \)

所以我们要维护四个树状数组:

  • d[i][j]
  • d[i][j] * i
  • d[i][j] * j
  • d[i][j] * i * j

posted on 2026-06-29 10:23  我不爱吃汉堡  阅读(2)  评论(0)    收藏  举报

导航