树状数组专题

树状数组

一、树状数组介绍

树状数组(也称为二叉索引树)是一种数据结构,它可以快速计算前缀和并支持 动态维护。与普通的前缀和数组相比,树状数组的 优势 在于它能够在O(logn)的时间复杂度内 更新单个元素的值,同时仍然能够在O(logn)的时间复杂度内 计算前缀和。这使得树状数组在 处理动态数据 时非常高效,以下是树状数组的常见使用场景及相关的简单例子:

  1. 单点更新与查询
    树状数组支持在指定位置 更新元素,并且很快地查询更新后的结果。

    • 例子:使用树状数组来计算数组nums=[0,0,0,0,0]中某个位置i之前的所有元素和。首先执行单点更新操作update(2,5),将位置2的元素更新为5。然后执行查询操作query(4),即查询位置4之前的元素和,结果为 0+0+5+0=5
  2. 区间更新与查询
    树状数组借助 差分思想 还可以 支持对指定区间内的元素进行更新,并且快速查询更新后的结果。

    • 例子:使用树状数组来计算数组nums=[0,0,0,0,0]中某个区间[l,r]内的所有元素和。首先执行区间更新操作rangeUpdate(2,4,2),即将位置2到位置4的元素都加2
  3. 数组逆序对统计
    树状数组也可以用于高效地计算数组中的逆序对个数。

    • 例子:给定数组nums=[5,2,6,1,3,4],使用树状数组来统计逆序对的数量。逆序对是指数组中的一对元素(i,j),其中 i<jnums[i]>nums[j]。经过计算得到逆序对的数量为 4
  4. 维护最大、最小值
    树状数组维护最大值的思路与维护前缀和类似。我们可以建立一个树状数组,其中每个节点存储它所代表的区间的最大值。在 更新 元素时,我们需要 沿着树状数组的路径向上更新 所有受影响的节点,以保证它们存储的最大值始终正确。在 查询 区间最大值时,我们可以将查询区间分解为若干个小区间,然后 查询这些小区间 在树状数组中对应节点的 最大值,最后 取所有查询结果的最大值 作为最终结果。

    树状数组维护区间最值

二、与普通前缀和区别

  • 树状数组: 较快的修改时间O(log2N)+较快的查询时间O(log2N)
  • 普通前缀和:较慢的修改时间O(N)+极快的查询时间O(1)

三、前置知识

lowbit()运算:非负整数x在二进制表示下最低位1及其后面的0构成的数值

举个栗子:
lowbit(12)=lowbit([1100]2)=[100]2=4

Code

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

四、树状数组结构

树状数组的本质思想是使用 树结构 维护 前缀和 ,从而把时间复杂度降为O(log2n)

① 每个节点t[x]保存以x为根的 子树中叶节点值的和
每个节点覆盖的长度 为lowbit(x)
t[x]节点的父节点为t[x + lowbit(x)]
树的深度为log2n+1

五、树状数组操作

  • add(x, k)表示将序列中第x个数加上k

add(3, 5)为例:
在整棵树上维护这个值,需要一层一层向上找到父节点,并将这些节点上的t[x]值都加上k,这样保证计算区间和时的结果正确。时间复杂度为O(log2n)

void add(int x, int k){
    for(int i = x; i <= n; i += lowbit(i)) t[i] += k;
}
  • ask(x)表示将查询序列前x个数的和

ask(7)为例:
查询这个点的前缀和,需要从这个点向左上找到上一个节点,将加上其节点的值。向左上找到上一个节点,只需要将下标 x -= lowbit(x),例如 7 - lowbit(7) = 6,6-lowbit(6)=4,4-lowbit(4)=0

lowbit(7)=1 0111 ->截取最后一个数字1,是1
lowbit(6)=2 0110 ->截取最后一个数字1,是2
lowbit(5)=1 0101 ->截取最后一个数字1,是1
lowbit(4)=4 0100 ->截取最后一个数字1,是4
lowbit(3)=1 0011 ->截取最后一个数字1,是1
lowbit(2)=2 0010 ->截取最后一个数字1,是2
lowbit(1)=1 0001 ->截取最后一个数字1,是1
int ask(int x){
    int sum = 0;
    for(int i = x; i; i -= lowbit(i))  sum += t[i];
    return sum;
}

题单

洛谷 P3374 【模板】树状数组 1
【基础模板】

HDU1166 敌兵布阵
【单点修改+区间和查询】

AcWing 788. 逆序对的数量 P1908 逆序对
【树状数组+逆序对+离散化,与上一题是同一题】

POJ 3067 Japan
【二维逆序对,结构体按一维由小到大,二维由小到大排序,不使用离散化】

P1966 [NOIP2013 提高组] 火柴排队
【逆序对,离散化,位置+高度,按高度由小到大排序,倒序枚举就是由高到低,逐个进入树状数组,不断取位置比我大,但在我左侧的数量】

POJ 2299 UltraQuickSort
【不管是否需要,一律原地静态数组离散化,由小到大排序,配合lower_bound,比我小,并且序号在我后面的统计个数】
【不用由大到小排序,那样配合lower_bound会出问题,不建议】

P2345 [USACO04OPEN] 奶牛集会
【两个树状数组,一个用于维护奶牛的坐标和,一个用于维护奶牛前后的个数,数学分析式子】

P3368 【模板】树状数组 2
【区间修改,单点查询,树状数组维护差分,求和就是变化值,sum(k)+a[k]

AcWing 242. 一个简单的整数问题
【区间修改,单点查询,树状数组维护差分,求和就是变化值,sum(k)+a[k]

LOJ 10117「一本通 4.1 练习 2」简单题
【区间修改,单点查询,树状数组维护差分,求和就是变化值,sum(k)+a[k]

LOJ 10115. 「一本通 4.13」校门外的树
【左右括号问题,两个树状数组,分别记录左括号个数,右括号个数】

AcWing 241 楼兰图腾
【树状数组+及时统计并用数组记录+动态单点修改】

POJ 2352 Stars
【扫描线+树状数组】


:其实【区间修改,区间查询】还得是线段树,用树状数组+推公式的办法也可以做,但不是正解:

AcWing 243. 一个简单的整数问题2
【区间修改、区间查询(利用差分+推公式)】

P3372 【模板】线段树1
【与上面的就是一个题】

POJ 3468 A Simple Problem with Integers
【与上面的就是一个题】

HDU 1754 I Hate It
【树状数组求最大最小值模板题】

POJ 3264 Balanced Lineup
【树状数组求最大最小值,在上面的题目上同时加上求最大和求最小】

AcWing 244. 谜一样的牛
【逆向思考+树状数组维护前缀和+二分快速查找sum=h[i]

HDU 2852 KiKis KNumber
【逆向思考+树状数组维护前缀和+二分快速查找】


二维树状数组

一、二维树状数组

二维树状数组,其实就是一维的树状数组上的节点再套个树状数组,变成了二维树状数组。

const int N = 1e3 + 10;
int c[N][N], n, m;

#define lowbit(x) (x & -x)

void add(int x, int y, int v) {
    for (int i = x; i <= n; i += lowbit(i))
        for (int j = y; j <= m; j += lowbit(j))
            c[i][j] += v;
}

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

二、单点修改,区间查询

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

给出一个 n×m 的零矩阵 A ,你需要完成如下操作:

  • 1xyk :表示元素 Ax,y 增加 k
  • 2abcd: 表示询问左上角为 (a,b) ,右下角为 (c,d) 的子矩阵内所有数的和

单点增加,因此可以直接加上就可以了

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

typedef long long LL;
const int N = 5000; // 2^(12)=4096

int n, m;

LL c[N][N];
#define lowbit(x) (x & -x)
void add(int x, int y, int d) {
    for (int i = x; i < N; i += lowbit(i))
        for (int j = y; j < N; j += lowbit(j))
            c[i][j] += d;
}
LL query(int x, int y) {
    LL res = 0;
    for (int i = x; i; i -= lowbit(i))
        for (int j = y; j; j -= lowbit(j))
            res += c[i][j];
    return res;
}

int main() {
    // 加快读入
    ios::sync_with_stdio(false), cin.tie(0);
    cin >> n >> m;
    int opt;
    while (cin >> opt) {
        if (opt == 1) {
            int x, y, d;
            cin >> x >> y >> d;
            add(x, y, d);
        } 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) << '\n';
        }
    }
    return 0;
}

三、区间修改,单点查询

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

给出一个 n×m 的零矩阵 A ,你需要完成如下操作:

  • 1abcdk:表示左上角为 (a,b) ,右下角为 (c,d) 的子矩阵内所有数都自增加 k
  • 2xy :表示询问元素 Ax,y 的值。

只需要利用一个二维树状数组,维护一个二维差分数组,单点查询即可。

#include <bits/stdc++.h>

using namespace std;
typedef long long LL;
const int N = 5000;
int n, m;

LL c[N][N];
#define lowbit(x) (x & -x)
void add(int x, int y, int v) {
    for (int i = x; i <= n; i += lowbit(i))
        for (int j = y; j <= m; j += lowbit(j))
            c[i][j] += v;
}
LL query(int x, int y) {
    LL res = 0;
    for (int i = x; i; i -= lowbit(i))
        for (int j = y; j; j -= lowbit(j))
            res += c[i][j];
    return res;
}

int main() {
    //加快读入
    ios::sync_with_stdio(false), cin.tie(0);

    cin >> n >> m;
    int op;
    while (cin >> op) {
        if (op == 1) {
            int x1, y1, x2, y2, d;
            cin >> x1 >> y1 >> x2 >> y2 >> d;
            //二维差分
            add(x1, y1, d);
            add(x1, y2 + 1, -d);
            add(x2 + 1, y1, -d);
            add(x2 + 1, y2 + 1, d);
        } else {
            int x, y;
            cin >> x >> y;
            cout << query(x, y) << '\n';
        }
    }
    return 0;
}

四、区间修改,区间查询

LOJ #135. 二维树状数组 3:区间修改,区间查询

给定一个大小为 N×M 的零矩阵,直到输入文件结束,你需要进行若干个操作,操作有两类:

  • 1abcdx,表示将左上角为 (a,b) ,右下角为 (c,d) 的子矩阵全部加上 x

  • 2abcd , 表示询问左上角为 (a,b) ,右下角为 (c,d) 为顶点的子矩阵的所有数字之和。

考虑前缀和 sum[i][j] 和 原数组 a , 差分数组 d 之间的关系。

首先sum[i][j]=x=1iy=1ja[x][y] (二维前缀和)

又由于a[x][y]=u=1xv=1yd[u][v] (差分数组与原数组关系)

所以:

sum[i][j]=x=1iy=1ju=1xv=1yd[u][v]

可以说是非常复杂了......

统计d[u][v]出现次数

  • a[1][1]a[i][j],d[1][1]全都要出现一次,所以有i×jd[1][1],即d[1][1]×i×j

  • a[1][1]a[i][j],d[1][2]出现了多少次呢?头脑中出现一个二维差分转原数组(本质就是一个原数组转二维前缀和)的图像:

    • i=1,j=1时, d[1][2]就没有出现
    • i=1,j=2时, d[1][2]出现1
    • ...
    • i=2,j=1时, d[1][2]就没有出现
    • i=2,j=2时, d[1][2]出现1
    • ...

总结一下:

  • d[1][2]×i×(j1)
  • d[2][1]×(i1)×j
  • d[2][2]×(i1)×(j1)
    等等……

所以我们不难把式子变成:

sum[i][j]=x=1iy=1j[d[x][y]×(i+1x)×(j+1y)]

展开得到:

sum[i][j]=x=1iy=1j[d[x][y]×(i+1)×(j+1)d[x][y]×x×(j+1)d[x][y]×(i+1)×y+d[x][y]×xy]

也就相当于把这个式子拆成了四个部分:
(i+1)(j+1)×x=1iy=1jd[x][y](j+1)×x=1iy=1j(d[x][y]x)(i+1)×x=1iy=1j(d[x][y]y)x=1iy=1j(d[x][y]xy)

所以我们需要在原来 C1[i][j] 记录 d[i][j] 的基础上,再添加三个树状数组:

C2[i][j] 记录 d[i][j]i
C3[i][j] 记录 d[i][j]j
C4[i][j] 记录 d[i][j]ij

这样一来,就能通过数组a[i][j]的差分数组d[i][j]来得到a[i][j]的前缀和数组sum[i][j]

最后,易知(x1,y1)(x2,y2)的矩阵和就是一个标准的二维前缀和公式,等于sum[x2][y2]sum[x2][y11]sum[x11][y2]+sum[x11][y11]

Code

#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 2050;

int n, m;
LL c1[N][N], c2[N][N], c3[N][N], c4[N][N];
#define lowbit(x) (x & -x)

// 维护四个树状数组
void add(int x, int y, int v) {
    for (int i = x; i < N; i += lowbit(i))
        for (int j = y; j < N; j += lowbit(j)) {
            c1[i][j] += v;
            c2[i][j] += v * x;
            c3[i][j] += v * y;
            c4[i][j] += v * x * y;
        }
}

// 查询左上角为(1,1)右下角为(x,y)的矩阵和
LL query(int x, int y) {
    LL res = 0;
    for (int i = x; i; i -= lowbit(i)) {
        for (int j = y; j; j -= lowbit(j)) {
            res += (x + 1) * (y + 1) * c1[i][j];
            res -= (y + 1) * c2[i][j];
            res -= (x + 1) * c3[i][j];
            res += c4[i][j];
        }
    }
    return res;
}

int main() {
    // 加快读入
    ios::sync_with_stdio(false), cin.tie(0);
    cin >> n >> m;
    int op;
    while (cin >> op) {
        int x1, y1, x2, y2;
        cin >> x1 >> y1 >> x2 >> y2;
        if (op == 1) {
            int d;
            cin >> d;
            // 维护四个数组
            add(x1, y1, d);
            add(x1, y2 + 1, -d);
            add(x2 + 1, y1, -d);
            add(x2 + 1, y2 + 1, d);
        } else
            cout << query(x2, y2) - query(x1 - 1, y2) - query(x2, y1 - 1) + query(x1 - 1, y1 - 1) << '\n';
    }
    return 0;
}

POJ 2155 Matrix
【二维树状数组】

posted @   糖豆爸爸  阅读(120)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
历史上的今天:
2020-04-13 Golang生成xlsx
2019-04-13 记录一次查看后台是否在运行资源备份上报到华为云存储的过程
2019-04-13 潭州课堂python
Live2D
点击右上角即可分享
微信分享提示