首页 > 其他分享 >树状数组

树状数组

时间:2024-02-18 21:12:31浏览次数:21  
标签:树状 int lowbit sum 数组 区间 operatorname

树状数组是一种支持 \(O(\log n)\) 时间内进行 单点修改区间查询 的,代码量小的数据结构。

原理及实现

下为 洛谷 P3374 【模板】树状数组 1 题意简述:

已知一个 长度为 \(n\) 的数列 \(a\),你需要进行下面两种操作:

  • 将某一个数加上 \(x\);

  • 求出某区间每一个数的和。

保证 \(n \le 5 \times 10 ^ {5}\)​。

\(\operatorname{lowbit}()\)

定义 \(\operatorname{lowbit}(x)\) 表示二进制下 \(x\) 最低的 \(1\) 对应的数。

例如 \(\operatorname{lowbit}(20) = \operatorname{lowbit}(10100_{(2)}) = 100_{(2)} = 4\)。

形式化地说,对于任意正整数 \(x\),总能将 \(x\) 表示成 \(s \times 2^{k + 1} + 2^k\),其中 \(2^k=\operatorname{lowbit}(x)\)。

在应用中,由于计算机中负数用补码表示,即各位取反后加 \(1\),所以 \(\operatorname{lowbit}(x)\) 可以快速地用 x & -x 求出,下面是一个例子。

原码:\(1010 \cdots 1000 \cdots 0\)

反码:\(0101 \cdots 0111 \cdots 1\)

补码:\(0101 \cdots 1000 \cdots 0\)​

显然有 lowbit(x) = x & -x

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

管辖区间

树状数组每个位置存储的是其向前 \(\operatorname{lowbit}\) 长度的区间和。形式化地说,设树状数组为 \(c\) ,那么 \(c_x = \sum_{i = x - \operatorname{lowbit}(x) + 1}^{x} a_i\)。我们称 \([x - \operatorname{lowbit}(x) + 1, x]\) 为 \(c_x\) 的管辖区间。

下图展示了树状数组的工作原理:

树状数组

区间查询

先不考虑如何求出 \(c\),先来考虑如何用 \(c\) 求出区间和。

假设要求 \(\sum_{i = 1}^x a_i\),设 \(t = x - \operatorname{lowbit}(x) + 1\),易得 \(\sum_{i = 1}^x a_i = \sum_{i = 1}^t a_i + \sum_{i = t + 1}^x a_i = \sum_{i = 1}^t a_i + c_x\)。

因此问题从求 \(\sum_{i = 1}^x a_i\) 变成了求 \(\sum_{i = 1}^{t} a_i\)。那么接下来对 \(t\) 进行类似的操作即可。

形式化地,我们可以写出查询 \(\sum_{i = 1}^x a_i\)​ 的过程:

  • 从 \(c_x\) 开始往前跳,有 \(c_x = \sum_{i = x- \operatorname{lowbit}(x) + 1}^{x} a_i\);
  • 令 \(x \gets x - \operatorname{lowbit}(x)\),如果 \(x = 0\) 说明已经跳到尽头了,终止循环;否则回到第一步。
  • 将跳到的 \(c\)​ 合并。

设 \(a_0 = 0\),类似前缀和,由此我们可以求出任意一个区间 \([l, r]\) 的区间和:

\[\sum_{i = l}^{r} a_i = \sum_{i = 1}^{r} a_i - \sum_{i = 1}^{l - 1} a_i \]

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

单点修改

假设要给 \(a_x\) 加上 \(k\),那么就找到并需要修改所有包含 \(a_x\) 的 \(c\)。

设 \(c_i\) 包含 \(a_x\),那么一定有:

  • \(x \le i\),这是显然的,因为每个 \(c\) 都不会包含后面的数。

  • \(\operatorname{lowbit}(x) \le \operatorname{lowbit}(i)\)​,当且仅当 \(x = i\) 时等号成立。

    观察 \(c_i\) 的管辖区间 \([i - \operatorname{lowbit}(i) + 1, i]\) 中每一个数的二进制表示,它们的 \(\operatorname{lowbit}\) 都比 \(i\) 更低。

    形式化证明:设 \(i = s \times 2^{k + 1} + 2^k\),其中 \(2^k = \operatorname{lowbit}(y)\)。那么 \(c_i\) 的管辖区间可以表示为 \([s \times 2^{k + 1} + 1, s \times 2^{k + 1} + 2^k]\)。这些数的 \(\operatorname{lowbit}\) 在 \([1, 2^k]\) 中,得证。

  • \(\operatorname{lowbit}\) 相同的 \(c\) 不会包含同一个数,换言之所有 \(i\) 的 \(\operatorname{lowbit}\) 都不同。由上一点的证明可知 \(\operatorname{lowbit}\) 不同的 \(c\) 管辖区间不会重叠。

由以上三点我们得知,可以按 \(\operatorname{lowbit}(i)\) 从小到大的顺序找出所有 \(i\)​。

\(x\) 是第一个 \(i\),记为 \(i_0\)。易证不存在 \(i > i_0\) 且 \(\operatorname{lowbit}(i) < \operatorname{lowbit}(i_0)\)。

在所有满足 \(i > i_0\) 且 \(\operatorname{lowbit}(i) > \operatorname{lowbit}(i_0)\) 的 \(i\) 中,\(i_0 + \operatorname{lowbit}(i_0)\) 的 \(\operatorname{lowbit}\) 显然是最小的,记其为 \(t\) 。易证 \(\operatorname{lowbit}(t) > \operatorname{lowbit}(i_0)\),因此 \(t - \operatorname{lowbit}(t) < i_0\),所以 \(c_t\) 包含 \(i_0\),即 \(c_t\) 包含 \(i\),所以 \(t = i_0 + \operatorname{lowbit}(i_0)\) 是第二个 \(i\),即为 \(i_1\)。

类似地,\(i_2 = i_1 + \operatorname{lowbit}(i_1)\)​,以此类推。

形式化地,我们可以写出给 \(a_x\) 加上 \(k\) 的过程:

  • 初始令 \(i = x\)。
  • \(c_i \gets c_i + k\)。
  • 令 \(i \gets i + \operatorname{lowbit}(i)\),如果 \(i > n\) 说明已经跳到尽头了,终止循环;否则回到第二步。
void add(int x, int k)
{
    for (int i = x; i <= n; i += lowbit(i))
        c[i] += k;
}

初始化

可以设 \(a\) 初始全部为 \(0\),那么 \(c\) 也全部为 \(0\),然后进行 \(n\)​ 次单点修改操作:

for (int i = 1; i <= n; i++)
{
    int x;
    std::cin >> x;
    add(i, x);
}

当然,也有一种更优雅的,时间复杂度为 \(O(n)\) 的方式:

int pre[MAXN]; // a 数组的前缀和
for (int i = 1; i <= n; i++)
{
    pre[i] = pre[i - 1] + a[i];
    c[i] = pre[i] - pre[i - lowbit(i)];
}

完整代码

#include <iostream>

int n;
int c[500005];

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

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

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

int main()
{
    std::ios::sync_with_stdio(false);
    std::cin.tie(nullptr);
    std::cout.tie(nullptr);

    int m;

    std::cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        int x;
        std::cin >> x;
        add(i, x);
    }

    for (int i = 1; i <= m; i++)
    {
        int op, x, y;
        std::cin >> op >> x >> y;
        if (op == 1)
            add(x, y);
        else
            std::cout << sum(y) - sum(x - 1) << "\n";
    }

    return 0;
}

复杂度分析

空间复杂度显然为 \(O(n)\)。

时间复杂度:单点修改和区间查询的过程中,每一步都是跳一个 \(\operatorname{lowbit}\),而 \(n\) 最多有 \(\log n\) 个二进制位,因此时间复杂度为 \(O(\log n)\)。

拓展

区间修改与单点查询

利用差分,树状数组还可以支持 区间修改单点查询 的操作。

易知这个问题可以转化为对原数列的差分数组进行单点修改和区间查询。

维护不是前缀和的信息

类似于单点加、求区间和,树状数组可以维护各种信息。

普通树状数组维护的信息及运算要满足 结合律可差分,如加法(和)、乘法(积)、异或等。

  • 结合律:\((x \circ y) \circ z = x \circ (y \circ z)\),其中 \(\circ\)​ 是一个二元运算符。

    区间查询 的本质是将目标区间拆成不超过 \(\log n\) 个区间并合并。不满足结合律的运算是不可以拆成几个部分再合并的。

  • 可差分:具有逆运算的运算,即已知 \(x \circ y\) 和 \(x\) 可以求出 \(y\)。

    对于不可差分信息,不存在直接修改 \(c\) 的方式。这是因为修改本身就相当于是把旧数从原区间「移除」,然后加入一个新数。「移除」时对区间信息的影响,相当于做「逆运算」,而不可差分信息不存在「逆运算」,所以无法直接修改 \(c\)​。

    (引用自 OI Wiki)

    事实上,通过一些修改,树状数组也可以维护不可差分信息,但复杂度劣于拥有同样功能的线段树,这里不作介绍。

标签:树状,int,lowbit,sum,数组,区间,operatorname
From: https://www.cnblogs.com/lzy20091001/p/18019951/fenwick

相关文章

  • 情书密码 (树状数组)
    题目问题描述有消息称:绝恋找到了自己的MissRight,正准备自己的表白。绝恋已经写好了情书,但为了避免其它人截获,他对情书进行加密。为了打探绝恋的私密,你冒着生命危险终于搞到了这封情书。原以为可以轻易将情书解密,结果竟然发现聪明的绝恋并没有直接写出加密用的密码,而是在那粉红......
  • 学习笔记#4:树状数组和 LIS
    学习笔记#4:树状数组和LIS前言:树状数组是和线段树类似的数据结构,更确切的说,树状数组能解决的问题,线段树都能解决,而线段树还能解决一些树状数组所不能解决的问题。因此线段树的应用范围比树状数组更广泛。但是,树状数组的常数更小,在同样的\(\text{O}(n\logn)\)复杂度下,树状数......
  • 用好内存从数组开始
    数组就是将相同数据的多个数据连续排列在内存中的一个元素序列。数组是使用内存的基础,数组之所以是使用内存的基础,是因为它反映的就是内存的物理结构本身,使用数组可以提高编程效率,在循环中使用数组可以用很短的代码按顺序读取或写入数组元素。栈和队列都是无需指定地址和下标就可......
  • 树状数组
    HH的项链看到这题,我首先想到了用flag[]数组标记,很明显这是必需的。但随着代码进行,又会遇到一个问题:如1212,如果按照正常标记后面两个就不会被标记,那遍历3到4时,结果为0。顺着思路想,如果我们在用完一次后把它丢掉,重新遍历,这也就导致我们必须要采用一种有序遍历,从而让后面的更......
  • 请编写函数fun,它的功能是:求出1到100之内能被7或者11整除, 但不能同时被7和11整除的所有
    /2.请编写函数fun,它的功能是:求出1到100之内能被7或者11整除,但不能同时被7和11整除的所有整数,并将他们放在a所指的数组中,通过n返回这些数的个数/#include<stdio.h>#include<string.h>intfun(int*buf){inti=1,j=0;for(i=1;i<100;i++){if(i%7==......
  • 1.m个人的成绩存放在score数组中,请编写函数fun, 它的功能是:将低于平均分的人数作为函
    /1.m个人的成绩存放在score数组中,请编写函数fun,它的功能是:将低于平均分的人数作为函数值返回,将低于平均分的分数放在below所指1定的数组中。/#include<stdio.h>#include<string.h>intfun(int*buf,int*buff,intnum){inti=0,j=0,sum=0;for(i=......
  • 1.1 - numpy数组的属性和创建
    1.1.1numpy数组Numpy(NumberPython)是Python进行科学计算的一个扩展库,提供了大量的函数和操作,主要用于对多维数组执行计算。Numpy数组中的每个元素都有相同的类型;并且数组大小是不可变的,修改数组大小将会创建新的数组。而python的列表类型list则会动态的扩展长度。1.1.......
  • 树状数组
    从这边抄(借鉴)的这是一个完整的二叉树把它变成直角三角形下面用一维数组对应删掉多余的叶子这个就是树状数组......
  • 数据结构【树状数组】
    树状数组是线段树的衍生产物,牺牲了部分通用性,节约了空间,且大大减少了手写码量。借助树状数组,我们可以用O(logN)的时间复杂度来实现给定序列中长度为n的区间中元素和的计算。https://www.bilibili.com/video/BV1ce411u7qP/?spm_id_from=333.337.search-card.all.click&vd_source......
  • 字符串、向量和数组
    一、字符串1.引入库include<string>usingstd::string;2.初始化strings(10,'c');//直接初始化strings1("hello");//直接初始化strings2="hello";//拷贝初始化3.操作(1)s+="world"//左值引用(返回值),避免拷贝(2)st......