树状数组

树状,数组 · 浏览次数 : 6

小编点评

**树状数组**是一种数据结构,用于维护序列的前缀和。它的基本思想是,建立一个新的数组,存储原数组的每个位置的子和数量。这样,我们可以快速地计算子数组的和、差值等信息。 **关键概念:** * **lowbit(x)**:计算 x 的最低有效二进制数。 * **add(x, c)**:在第 x 个位置增加值 c。 * **sum(x)**:计算前 x 个元素的和。 * **区间查询**:计算以 x 为开始、以 y 为结束的数组元素的和。 **基本操作:** 1. **构建树状数组**:对于每个位置 x,记录该位置的子和数量。 2. **单点查询**:计算以 x 为开始、以 y 为结束的数组元素的和。 3. **区间查询**:计算以 x 为开始、以 y 为结束的数组元素的和。 **应用:** * **单点修改**:增加或删除原数组中的特定位置的值。 * **区间修改**:对特定范围内的元素进行加减或乘法。 * **区间查询**:计算特定范围内的元素的和或差值。 **例题:** ``` 输入: 10 51 2 3 4 5 6 7 8 9 10 操作: C 4 1 6 3 Q 2 输出: 4125 ``` **复杂度:** * **单点查询**:O(log(n)),其中 n 是数组长度。 * **区间查询**:O(log(n))。 * **区间修改**:O(n),其中 n 是数组长度。 **注意:** * 树状数组是动态数组,需要在使用前动态分配内存。 * 树状数组的构建需要 O(n) 的时间,其中 n 是数组长度。

正文

都说树状数组思路很难,那我们今天就给他讲个透彻!
前置知识:lowbit 运算
lowbit 的作用就是返回一个数从右往左数的第一个1与他前面所有的0所组成的十进制数
举个例子:
\(114\)这个数转换为二进制为\(1110010\),而它从右往左数的第一个\(1\)在第二位,将这位右边的所有\(0\)放出来为\(10\),转换为十进制为\(2\),所以 lowbit(114) 返回\(2\)
lowbit的代码:

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

树状数组的思路

树状数组的基本作用就是维护一个序列的前缀和,如下图:
在这里插入图片描述
我们先把每个节点下标的二进制数写出来,如下:
在这里插入图片描述
我们可以发现,树状数组有如下性质(注意以下的 xlowbit(x) 均为十进制数):

  1. 每个内部节点 c[x] 保存的是以它为根的子树中所有叶节点的和
  2. 每个内部节点 c[x] 的子节点个数为 lowbit(x) 的位数
  3. 除根节点外,每个内部节点 c[x] 的父节点为 c[x + lowbit(x)](下面会经常用到)
  4. 树的深度为 \(O(log \ n)\)

接下来我们看看如何对树状数组进行操作

树状数组的操作


单点修改

单点修改:把数列中第 \(x\) 个数加 \(d\)
因为我们在子节点增加的值需要向上传递,所以我们这么写修改:

void add(int x, int c) {
    for (int i = x; i <= n; i += lowbit(i))
        tr[i] += c;
    //	每个内部节点 tr[i] 的父节点为 tr[i + lowbit(i)]
}

初始化树状数组

建立一个全为 \(0\) 的数组 tr , 然后对每个位置 x 执行 add(x, a[x]) 即可。

for (int i = 1; i <= n; i++)
    add(i, a[i]);

区间查询

区间查询:求区间的前 \(x\) 项的和(也就是前缀和)
查询的时候我们就每次减掉 lowbit(i) 再相加就可以啦

int sum(int x) {
    int res = 0;
    for (int i = x; i; i -= lowbit(i))
        res += tr[i];
    return res;
}

还有另外一种:求数列中第 \(l \sim r\) 个数的和,这时候我们就可与利用前缀和的性质,即 \(sum[l, r] = sum[1, r] - sum[1, l - 1]\)

cout << sum(r) - sum(l - 1) << '\n';

区间修改

区间查询:把数列中第 \(l \sim r\) 个数都加 \(d\)
对于区间查询这个操作,我们需要用树状数组维护原序列 a 的差分数组 b,如下:

for (int i = 1; i <= n; i++)
    add(i, a[i] - a[i - 1]); //	add函数在上面有

由于差分数组的性质,我们想要将区间 \([l, r]\) 加上 \(d\) ,就相当于 \(add(l, d)\)\(add(r + 1, -d)\)

add(l, d), add(r + 1, -d);

单点查询

\(a[x]\) 就相当于求 \(b[1] + b[2] + b[3] + ... + b[x]\),也就是树状数组 \(1 \sim x\) 的和,直接使用上面的 sum 函数即可

int sum(int x) {
    int res = 0;
    for (int i = x; i; i -= lowbit(i))
        res += tr[i];
    return res;
}

例题

给定长度为 \(N\) 的数列 \(A\),然后输入 \(M\) 行操作指令。

第一类指令形如 C l r d,表示把数列中第 \(l \sim r\) 个数都加 \(d\)

第二类指令形如 Q x,表示询问数列中第 \(x\) 个数的值。

对于每个询问,输出一个整数表示答案。

输入格式

第一行包含两个整数 \(N\)\(M\)

第二行包含 \(N\) 个整数 \(A[i]\)

接下来 \(M\) 行表示 \(M\) 条指令,每条指令的格式如题目描述所示。

输出格式

对于每个询问,输出一个整数表示答案。

每个答案占一行。

数据范围

\(1 \le N,M \le 10^5\),
\(|d| \le 10000\),
\(|A[i]| \le 10^9\)

输入样例:

10 5
1 2 3 4 5 6 7 8 9 10
Q 4
Q 1
Q 2
C 1 6 3
Q 2

输出样例:

4
1
2
5

这题就属于上面讲解树状数组维护差分数组那一类的,代码:

#include <algorithm>
#include <cstdio>
#include <cstring>
#include <iostream>

using namespace std;

typedef long long ll;

const int N = 100010;

int n, m;
int a[N];
ll tr[N];

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

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

ll sum(int x) {
    ll sum = 0;
    for (int i = x; i; i -= lowbit(i))
        sum += tr[i];
    return sum;
}

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

    for (int i = 1; i <= n; i++)
        add(i, a[i] - a[i - 1]);

    while (m--) {
        char op[2];
        int l, r, d;
        scanf("%s%d", op, &l);
        if (*op == 'C') {
            scanf("%d%d", &r, &d);
            add(l, d), add(r + 1, -d);
        } else
            printf("%lld\n", sum(l));
    }

    return 0;
}

最后说一句:对于那些需要即需要区间查询又需要区间修改的题,还是建议直接使用线段树,可以参考一下我的线段树讲解:C++线段树详解

与树状数组相似的内容:

树状数组

都说树状数组思路很难,那我们今天就给他讲个透彻! 前置知识:`lowbit` 运算 `lowbit` 的作用就是返回一个数从右往左数的第一个1与他前面所有的0所组成的十进制数 举个例子: $114$这个数转换为二进制为$1110010$,而它从右往左数的第一个$1$在第二位,将这位右边的所有$0$放

深入理解树状数组

树状数组(BIT, Binary Indexed Tree)是简洁优美的数据结构,它能在很少的代码量下支持单点修改和区间查询,我们先以a[] {1, 2, 3, 4, 5, 6}数组为例建立树状数组看一下树状数组的样子:

LeetCode 双周赛 103(2023/04/29)区间求和的树状数组经典应用

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 大家好,我是小彭。 这场周赛是 LeetCode 双周赛第 103 场,难得在五一假期第一天打周赛的人数也没有少太多。这场比赛前 3 题比较简单,我们把篇幅留给最后一题。 往期周赛回顾:LeetCode 单周

树链剖分[学习笔记]

树链剖分 壹. 树剖,就是树链剖分,将一棵树剖分成一堆链 (如说 \(\dots\) ) 本文主要介绍重链剖分。 树剖成链之后一段重链上的 \(dfs\) 序是连续的,那么我们就可以对 \(dfs\) 序使用一些数据结构(树状数组、线段树等) \(1\).一些变量及意义 \(fa[x]\) \(x\

Merkle Tree 简介

Merkle 树(Merkle Tree)是一种树状数据结构,通常用于验证大规模数据集的完整性和一致性。它的名字来源于其发明者 Ralph Merkle。Merkle 树在密码学、分布式系统和区块链等领域得到广泛应用,尤其在区块链中,它用于验证交易和区块的完整性,确保数据不被篡改。 下面是 Merk

[转帖]LSM树详解

https://zhuanlan.zhihu.com/p/181498475 LSM树(Log-Structured-Merge-Tree)的名字往往会给初识者一个错误的印象,事实上,LSM树并不像B+树、红黑树一样是一颗严格的树状数据结构,它其实是一种存储结构,目前HBase,LevelDB,Ro

动态开点线段树说明

动态开点线段树说明 作者:Grey 原文地址: 博客园:动态开点线段树说明 CSDN:动态开点线段树说明 说明 针对普通线段树,参考使用线段树解决数组任意区间元素修改问题 在普通线段树中,线段树在预处理的时候,需要申请 4 倍大小的数组空间来存放划分的区域, 而本文介绍的动态开点线段树,它和普通线段

leetcode 将有序数组转换为二叉搜索树

给你一个整数数组 nums ,其中元素已经按 升序 排列,请你将其转换为一棵 高度平衡 二叉搜索树。 高度平衡 二叉树是一棵满足「每个节点的左右两个子树的高度差的绝对值不超过 1 」的二叉树。 示例 1: 输入:nums = [-10,-3,0,5,9] 输出:[0,-3,9,-10,null,5]

前端开发中的二分查找算法

在前端开发中,处理和搜索大量数据时,效率是一个关键因素。二分查找算法是一种高效的查找算法,适用于在有序数组中快速找到目标值。本文将详细介绍二分查找算法的基本原理、实现方法及其在前端开发中的应用。 什么是二分查找? 二分查找(Binary Search)是一种在有序数组中查找目标值的算法。它通过不断将

[转帖]各种数据结构性能的比较

数据结构包括数组、链表、栈、二叉树、哈希表等等 数据结构优点缺点数组插入快查找慢、删除慢、大小固定有序数组查找快插入慢、删除慢、大小固定栈后进先出存取其他项很慢队列先进先出存取其他项很慢链表插入、删除快查找慢二叉树查找、插入、删除快算法复杂(删除算法)红黑树查找、插入、删除快算法复杂hash表存取极