树状数组详解

一、引入和概念

平常我们会遇到一些对数组进行维护查询的操作,比较常见的,修改某点的值、求某个区间的和。

数据规模不大的时候,对于修改某点的值是非常容易的,复杂度是O(1),但是对于求一个区间的和就要扫一遍了,复杂度是O(N)。

如果实时的对数组进行M次修改或求和,最坏的情况下复杂度是O(M*N),当规模增大后这是划不来的。

而树状数组干同样的事复杂度却是O(M*lgN)。

树状数组是一个查询和修改复杂度都为log(n)的数据结构。

主要用于查询任意两位之间的所有元素之和,但是每次只能修改一个元素的值;经过简单修改可以在log(n)的复杂度下进行范围修改,但是这时只能查询其中一个元素的值(如果加入多个辅助数组则可以实现区间修改与区间查询)。

看完概念后发现和线段树的功能类似,实际上树状数组和线段树确实类似,不过也有不同,具体区别和联系如下:

1.两者在复杂度上同级, 但是树状数组的常数明显优于线段树, 其编程复杂度也远小于线段树.

2.树状数组的作用被线段树完全涵盖, 凡是可以使用树状数组解决的问题, 使用线段树一定可以解决, 但是线段树能够解决的问题树状数组未必能够解决.

3.树状数组的突出特点是其编程的极端简洁性, 使用lowbit技术可以在很短的几步操作中完成树状数组的核心操作,与之相关的便是其代码效率远高于线段树。

二、实现

树状数组,重点是在树状的数组

一颗普通的二叉树如下

叶子结点代表A数组A[1]~A[8]

现在变形一下

现在定义每一列的顶端结点C[]数组 ,如下图

C[i]代表 子树的叶子结点的权值之和// 这里以求和举例

如图可以知道

C[1]=A[1];

C[2]=A[1]+A[2];

C[3]=A[3];

C[4]=A[1]+A[2]+A[3]+A[4];

C[5]=A[5];

C[6]=A[5]+A[6];

C[7]=A[7];

C[8]=A[1]+A[2]+A[3]+A[4]+A[5]+A[6]+A[7]+A[8];

其中C数组的求法如下:

C[i]=A[i-2^k+1]+A[i-2^k+2]+......A[i]; (k为i的二进制中从最低位到高位连续零的长度)例如i=8时,k=3;

为了更好的理解上面的公式,将1-32的2^k计算出来如下:

有了这个表格之后对照上面的式子就可以轻松的知道每一个C代表的哪几个数的和。比如

C[8],由上图知2^k为8,那么

C[24],由上图知2^k为8,那么

这是我们通过简单的计算得出来的值,那么应该如何转换成编程语言呢,大神们给出了非常巧妙的方法,利用下面的函数可以求出2^k的值。

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

为什么这样可以呢?这里复制了一篇证明可以看一下。

首先明白一个概念,计算机中-i=(i的取反+1),也就是i的补码 
而lowbit,就是求(树状数组中)一个数二进制的1的最低位,例如01100110,lowbit=00000010;再例如01100000,lowbit=00100000。 
所以若一个数(先考虑四位)的二进制为abcd,那么其取反为(1-a)(1-b)(1-c)(1-d),那么其补码为(1-a)(1-b)(1-c)(2-d)。 
如果d为1,什么事都没有-_-|||但我们知道如果d为0,天理不容2Σ( ° △ °|||)︴ 
于是就要进位。如果c也为0,那么1-b又要加1,然后又有可能是1-a……直到碰见一个为补码为0的bit,我们假设这个bit的位置为x 
这个时候可以发现:是不是x之前的bit的补码都与其自身不同?,x之后的补码与其自身一样都是0? 
例如01101000,反码为10010111,补码为10011000,可以看到在原来数正数第五位前,补码的进位因第五位使其不会受到影响,于是0&1=0,; 
但在这个原来数“1”后,所有零的补码都会因加1而进位,导致在这个“1”后所有数都变成0,再加上0&0=0,所以他们运算结果也都是零; 
只有在这个数处,0+1=1,连锁反应停止,所以这个数就被确定啦O(∩_∩)O

有了上面的基础,我们就可以解决很多问题了。重要的操作有两个,分别是更新和求和。

1.更新操作

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

2.求和操作

int getsum(int x)
{
    int ans=0;
    for(int i=x;i;i-=lowbit(i))//i要大于0
        ans+=C[i];
    return ans;
}

三、代码

#include <iostream>
#include <cstdlib>
#include <algorithm>
#include <string>
#include <cstring>
#include <stdio.h>
#include <queue>
#define IO ios::sync_with_stdio(false);\
    cin.tie(0);    cout.tie(0);
using namespace std;
#define N 50100
int n;
int c[N];
int lowbit(int x)
{
    return x&(-x);
}
void update(int k,int x)
{
    for(int i=k; i<=n; i+=lowbit(i))
        c[i]+=x;
}
int getsum(int x)
{
    int ans=0;
    for(int i=x; i; i-=lowbit(i)) //i要大于0
        ans+=c[i];
    return ans;
}
int main()
{
    IO;
    int T;
    cin>>T;
    int logo=1;
    while(T--)
    {
        memset(c,0,sizeof(c));
        cin>>n;
        for(int i=1; i<=n; i++)
        {
            int t;
            cin>>t;
            update(i,t);
        }
        char s[100];
        cout<<"Case "<<logo++<<":"<<endl;
        while(1)
        {
            cin>>s;
            if(s[0]==‘E‘)
                break;
            if(s[0]==‘Q‘)
            {
                int a,b;
                cin>>a>>b;
                cout<<getsum(b)-getsum(a-1)<<endl;
            }
            if(s[0] == ‘S‘)
            {
                int a,b;
                cin>>a>>b;
                update(a,-b);
            }
            if(s[0] == ‘A‘)
            {
                int a,b;
                cin>>a>>b;
                update(a,b);
            }
        }
    }
    return 0;
}

原文地址:https://www.cnblogs.com/aiguona/p/8278846.html

时间: 2024-10-18 22:35:46

树状数组详解的相关文章

BIT 树状数组 详解 及 例题

(一)树状数组的概念 如果给定一个数组,要你求里面所有数的和,一般都会想到累加.但是当那个数组很大的时候,累加就显得太耗时了,时间复杂度为O(n),并且采用累加的方法还有一个局限,那就是,当修改掉数组中的元素后,仍然要你求数组中某段元素的和,就显得麻烦了.所以我们就要用到树状数组,他的时间复杂度为O(lgn),相比之下就快得多.下面就讲一下什么是树状数组: 一般讲到树状数组都会少不了下面这个图: 下面来分析一下上面那个图看能得出什么规律: 据图可知:c1=a1,c2=a1+a2,c3=a3,c4

高级数据结构:优先队列、图、前缀树、分段树以及树状数组详解

优秀的算法往往取决于你采用哪种数据结构,除了常规数据结构,日常更多也会遇到高级的数据结构,实现要比那些常用的数据结构要复杂得多,这些高级的数据结构能够让你在处理一些复杂问题的过程中多拥有一把利器.同时,掌握好它们的性质以及所适用的场合,在分析问题的时候回归本质,很多题目都能迎刃而解了. 这篇文章将重点介绍几种高级的数据结构,它们是:优先队列.图.前缀树.分段树以及树状数组. 一.优先队列 1.优先队列的作用 优先队列最大的作用是能保证每次取出的元素都是队列中优先级别最高的,这个优先级别可以是自定

树状数组详解(图形学算法)

目录 一.从图形学算法说起 1.Median Filter 概述 2.r pixel-Median Filter 算法 3.一维模型 4.数据结构的设计 5.树状数组华丽登场 二.细说树状数组 1.树 or 数组? 2.结点的含义 3.求和操作 4.更新操作 5.lowbit函数O(1)实现 6.小结 三.树状数组的经典模型 1.PUIQ模型 2.IUPQ模型 3.逆序模型 4.二分模型 5.再说Median Filter 6.多维树状数组模型 四.树状数组题集整理 一.从图形学算法说起 1.M

树状数组略解

树状数组 树状数组是一个很奇特的树,它的节点会比线段树少一些,也能表示一个数组. 比如一个数组叫做a有8个数,那么它的树状数组样子就长这样 c数组就是树状数组,能看出来 c1=a1; c2=a1+a2; c3=a3; c4=a1+a2+a3+a4; 以此类推...... 很难说出他们的关系,但是如果把它们变为二进制 c0001=a0001 c0010=a0001+a0010 c0011=a0011 c0100=a0001+a0010+a0011+a0100 你会发现,将每一个二进制,去掉所有高位

oracle树状索引详解(图摘取《收获不止oracle》)

一.树状索引特点 1.高度较低 2.存储列值 3.结构有序 我们先看一下索引的结构,如图: 以上结构图说明索引是由 ROOT(根块),Branch(茎块)和Leaf(叶子块)三部分组成的,其中最底层的叶子块 主要存储了 key column value(索引列具体值),以及能具体定位到数据所在位置的rowid(此处rowid和查询时候用的rownum不是同一个概念,有兴趣可以百度rownum和rowid的区别) 注意点:索引块和数据块 需要区分,索引块也是占磁盘空间的 二.oracle索引查询

一维 + 二维树状数组 + 单点更新 + 区间更新 详解

树状数组详解: 假设一维数组为A[i](i=1,2,...n),则与它对应的树状数组C[i](i=1,2,...n)是这样定义的: C1 = A1 C2 = A1 + A2 C3 = A3 C4 = A1 + A2 + A3 + A4 C5 = A5 C6 = A5 + A6 ................. C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8 ................ 如图可知: 为奇数的时候他是代表他本身,而为偶数的时候则是代表着自

初学树状数组

原理: 有好的博客做讲解了(见参考文章),这里暂时略过,如果以后有新的理解和体会会再来写的.(应该不会) 思想: 这里可以把树状数组的精妙之处提一下(我理解的) 首先,树状数组之所以叫树状数组,因为它像树一样,有类似树的父子节点关系,这点在更新和求和操作上体现的最为明显.而最终也只是数组,因为实现起来简单方便,如数组一样.(一开始还纳闷为什么不叫二进制索引树),英文名BIT(Binary Index Tree).这个数据结构实现的功能像线段树一样,两者有着异曲同工之妙. 其次,树状数组的神奇之处

P3605 [USACO17JAN]Promotion Counting晋升者计数 线段树合并 or 树状数组

题意:每个点有一个权值    求每个节点的子树中比其权值大的节点数 线段树合并模板题 #include<bits/stdc++.h> using namespace std; #define rep(i,a,b) for(int i=(a);i<=(b);i++) #define repp(i,a,b) for(int i=(a);i>=(b);--i) #define ll long long #define see(x) (cerr<<(#x)<<'='

树状数组的改段求段详解

以下是对于如何利用树状数组进行区间修改和区间查询的简介 可以代替不需要lazy tag的线段树,且代码量和常数较小 首先你需要学会树状数组,如果不会的话以下先讲解黑匣子使用树状数组的姿势 首先定义一个数组 int c[N]; 并清空 memset(c, 0, sizeof c); 1.单点修改 : c[x] += y; 对应的函数是 change(x, y); 2.求前缀和 :  对应的函数是 int sum(x) 两种操作的复杂度都是O(logn) 模版如下 int c[N], maxn; i