浅析树状数组

目录

  • beginning

    • 顺序结构 A+B
    • 高精 A+B
    • 压位高精 A+B
    • 二分A+B
  • 树状数组简介(不喜欢啰嗦的请直接跳到这里)
    • 基础概念
    • 代码实现
      • 大体结构
      • lowbit
      • lowbit的作用
      • 总结+代码
    • 逆序对
      • 离散化
      • 方式
      • 实现
      • 代码
  • 树状数组进阶
    • 差分
    • 区间修改+单点查询
      • 主要思想
      • 单点查询
      • 区间修改
    • 区间修改+区间查询
      • 差分分析
      • 代码
  • 2D树状数组
    • query
    • update
    • 区间修改+单点查询
    • 区间修改+区间查询
    • 时间复杂度
  • 罗列例题
    • 一维
  • beginning
    • 顺序结构 A+B
    • 高精 A+B
    • 压位高精 A+B
    • 二分A+B
  • 树状数组简介(不喜欢啰嗦的请直接跳到这里)
    • 基础概念
    • 代码实现
      • 大体结构
      • lowbit
      • lowbit的作用
      • 总结+代码
    • 逆序对
      • 离散化
      • 方式
      • 实现
      • 代码
  • 树状数组进阶
    • 差分
    • 区间修改+单点查询
      • 主要思想
      • 单点查询
      • 区间修改
    • 区间修改+区间查询
      • 差分分析
      • 代码
  • 2D树状数组
    • query
    • update
    • 区间修改+单点查询
    • 区间修改+区间查询
    • 时间复杂度
  • 罗列例题
    • 一维

---恢复内容开始---

@(浅析树状数组——B.I.T.)


膜拜第

个来访本蒟蒻博客的大佬

beginning

让我们先从这道题开始:P1001 A+B Problem
说明: 我不是在搞笑 !
我们可以探讨一下,用我们所学的知识可以如何解决这道题.

顺序结构 A+B

#include<cstdio>
int a,b;
int main()
{
    scanf("%d%d",&a,&b);
    printf("%d",a+b);
}

明显没有任何毛病…… 傻子才会说有毛病

高精 A+B

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
    static const int LEN=100001;
    static const int ZR='0';
    int a[LEN];
    bigint() { a[0]=1,a[1]=0; }
    void operator=(char*s) {
        memset(a,0,sizeof(int)*a[0]);
        a[0]=S(s);
        for(int i=0;i<a[0];i++)
            a[a[0]-i]=s[i]-'0';
    }
    void operator=(int s) {
        char t[LEN];
        sprintf(t,"%d",s);
        *this=t;
    }
    bigint(int n) { *this=n; }
    bigint(char *s) { *this=s; }
    void in() {
        char s[LEN];
        scanf("%s",s);
        a[0]=S(s);
        for(int i=0;i<a[0];i++)
            a[a[0]-i]=s[i]-'0';
    }
    void out() {
        for(int i=a[0];i>=1;i--)
            printf("%d",a[i]);
    }
    bigint operator+(const bigint&s) {
        bigint ans;
        ans.a[0]=mx(a[0],s.a[0]);
        int x=0;
        for(int i=1;i<=ans.a[0];i++) {
            ans.a[i]=a[i]+s.a[i]+x;
            x=ans.a[i]/10;
            ans.a[i]%=10;
        }
        while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
        return ans;
    }
};
bigint a,b,c;
int main() {
    a.in();
    b.in();
    c=a+b;
    c.out();
}

压位高精 A+B

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
    static const int LEN=101;
    int a[LEN];
    bigint(){}
    void operator=(char*s) {
        memset(a,0,sizeof(a));
        int len=S(s);
        a[0]=len/4;
        if(len%4) a[0]++;
        for(int i=0,j;i<len&&(j=(len-i-1)/4+1);i++)
            a[j]=a[j]*10+s[i]-'0';
    }
    void operator=(int s) {
        char t[LEN];
        sprintf(t,"%d",s);
        *this=t;
    }
    bigint(int n) { *this=n; }
    bigint(char *s) { *this=s; }
    void in() {
        char s[LEN];
        scanf("%s",s);
        *this=s;
    }
    void out() {
        printf("%d",a[a[0]]);
        for(int i=a[0]-1;i>=1;i--)
            printf("%04d",a[i]);
    }
    bigint operator+(const bigint&s) {
        bigint ans=0;
        ans.a[0]=mx(a[0],s.a[0]);
        for(int i=1;i<=ans.a[0];i++) {
            ans.a[i]+=a[i]+s.a[i];
            if(ans.a[i]>=10000)
                ans.a[i]-=10000,ans.a[i+1]++;
        }
        while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
        return ans;
    }
};
bigint a,b,c;
int main() {
    a.in();
    b.in();
    c=a+b;
    c.out();
}

二分A+B

#include<cstdio>
inline int read()
{
    int a=0; char f=1,c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-') f=-f;
        if(c==-1) return -1;
        c=getchar();
    }
    while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
    return a*f;
}
inline int write(int a)
{
    if(a<0) a=(~a)+1;
    if(a/10) write(a/10);
    return putchar(a%10|48);
}
int a=read(),b=read(),l=-2e9,r=2e9;
int main()
{
    while(l+1<r)
    {
        int mid=(l+r)>>1;
        if(mid<a+b) l=mid;
        else r=mid;
    }
    write(r);
}

或许很多人已经觉得二分A+B已经很神奇了,但是我们今天要学习一种新的做A+B的方法:树状数组(B.I.T).

树状数组简介(不喜欢啰嗦的请直接跳到这里)

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

经过了如此一番看不懂的说明,或许你会直接绝望掉,But这东西贼重要,而且 这种东西竟然没有STL!!!气不气 QAQ

基础概念

假设数组$A_{1...n}$,那么查询$\sum_{j=1}^{i}A_j$的时间是$log$级别的,而且这是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为$log$级别。

来观察这个图:
令这棵树的结点编号为$C_1,C_2,C_3......C_n$。令每个结点的值为这棵树的值的总和,那么容易发现:

C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

这里有一个有趣的性质:
设节点编号为x,那么这个节点管辖的区间为$2^k$(其中k为x二进制末尾0的个数)个元素。

这样说是不是就要明确一些了?

根据这个性质,就有一个千古大罪人发明了树状数组

代码实现

大体结构

struct B_I_T{
    int C[100001];
    int n;
    ......//函数
};

lowbit

时间复杂度

$O(1)$

实现

还记得上面说的那一个性质吗?借助C++强大的位运算,我们可以在O(1)时间内求出$2^k$

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

还有一种更简单也更常用的方式,是这样的

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

struct B_I_T{
    int C[100001];
    int n;
    inline int lowbit(int x) { return x & -x; }
    ......//函数
};

就可以在结构体中加入这样一个函数了.
不过,更常用的方法不是写在结构体里面,而是写在外面,否则不在结构体当中就只能用B_I_T::lowbit(x) 了,即

inline int lowbit(int x) { return x & -x; }
struct B_I_T{
    int C[100001];
    int n;
    ......//函数
};

其实还有一种实现方法,用宏定义:#define lowbit(x) (x & -x)

lowbit的作用

update

时间复杂度

$O(log n)$

意义

$update(x,val)$ => $A_x=A_x+val$

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

这种使用for循环的做法,和下面使用while循环的原理是一样的.

void update(int x,int val) {
    while(x<=n) {
        C[x]+=val;
        x+=lowbit(x);
    }
}

假设n=8,执行update(3,5),则有如下流程

x=3 C[3]+=5 x=3+lowbit(3)=4
x=4 C[4]+=5 x=4+lowbit(4)=8
x=8 C[8]+=5 x=8+lowbit(8)=16
x=16 退出

对照上文的图片 我也不知道有多上文 我们可以知道,每一次C数组中执行加操作的下标,刚好都包括了$A_3$!

作用

作用1——初始化

inline void init() {
    scanf("%d",&n);
    for(reg int i=1;i<=n;i++) {
        scanf("%d",&a);
        update(i,a);
    }
}

作用2——改变单点的值 话说就是拿来干这件事的就不讲了

query

时间复杂度

$O(log n)$

意义

$query(x)$ => $A_1+A_2+……+A_x$

实现
int query(int x) {
    int res=0;
    for(reg int i=x;i;i-=lowbit(x)) res+=C[i];
    return res;
}

这种使用for循环的做法,和下面使用while循环的原理是一样的.

int query(int x) {
    int res=0;
    while(x) {
        res+=C[x];
        x-=lowbit(x);
    }
    return res;
}

假设n=8,执行query(7),则有如下流程

res=0
x=7 res=C[7]
x=6 res=C[6]+C[7]
x=4 res=C[4]+C[6]+C[7]
x=0 退出

仍然对照上(?)表,可以知道$C_4=A_1+A_2+A_3+A_4$,$C_6=A_5+A_6$,$C_7=A_7$。所以$C_4+C_6+C_7=\sum^7_{i=1}A_i$!

作用

作用1——求单点前缀和 不多赘述,直接$query(x)$即可
作用2——求区间和 $Sum(l,r)=query(r)-query(l-1)$,即r的前缀和减去l-1的前缀和,即为l->r的区间和

总结+代码

将以上的所有总结在一起,可以有如下代码

struct B_I_T{
    int n;
    int C[MAXN];
    inline int lowbit(int x) {
        return x & -x;
    }
    inline void update(int x,int val) {
        for(register int i=x;i<=n;i+=lowbit(i))
            C[i]+=val;
    }
    inline int query(int x) {
        int s=0;
        for(register int i=x;i;i-=lowbit(i))
            s+=C[i];
        return s;
    }
    inline void init() {
        for(register int i=1;i<=n;i++) {
            int a;
            scanf("%d",&a);
            update(i,a);
        }
    }
};

如此,我们就可以愉快地完成开头的问题A+B problem了!

逆序对

话说这不一道归并的题吗?拿到B.I.T.这里来干哈的?
先无脑打上mergesort的代码一份

int n,a[500001];
int s[500001];
long long ans;
void mrg(int left,int right,int mid)
{
    int i=left,j=mid+1,k=left;
    while(i<=mid&&j<=right)
    {
        if(a[i]>a[j])
        {
            ans+=mid-i+1;
            s[k++]=a[j++];
        }
        else s[k++]=a[i++];
    }
    while(i<=mid)
        s[k++]=a[i++];
    while(j<=right)
        s[k++]=a[j++];
    for(int i=left;i<=right;i++)
        a[i]=s[i];
}
void mergesort(int left,int right)
{
    if(left==right)return;
    int mid=(left+right)/2;
    mergesort(left,mid);
    mergesort(mid+1,right);
    mrg(left,right,mid);
}

离散化

喂喂喂,这又是什么鬼?又关这道题什么事?不要急,先听我说。

离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。
有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。比如当你数据个数n很小,数据范围却很大时(超过1e9)就考虑离散化更小的值,能够实现更多的算法。

Set an example:

方式

离散化常见的两种方式: 1、数组离散化 2、用STL+二分离散化

数组法

for(register int i=1;i<=n;i++)
{
    cin>>a[i].val;
    a[i].id = i;
}
sort(a+1,a+n+1);//定义结构体时按val从小到大重载
for(int i=1;i<=n;i++)
    b[a[i].id]=i;//将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值

STL+二分

//n原数组大小   num原数组中的元素    lsh离散化的数组    cnt离散化后的数组大小
int lsh[MAXN],cnt,num[MAXN],n;
for(int i=1;i<=n;i++)
{
    scanf("%d",&num[i]);
    lsh[i]=num[i];//复制一份原数组
}
sort(lsh+1,lsh+n+1);//排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
//cnt就是排序去重之后的长度
cnt=unique(lsh+1,lsh+n+1)-lsh-1;//unique返回去重之后最后一位后一位地址-数组首地址-1
for(int i=1;i<=n;i++)
    num[i]=lower_bound(lsh+1,lsh+cnt+1,num[i])-lsh;
//lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址-数组首地址,从而实现离散化

实现

逆序对实际上就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数垒加就是逆序对总数。


代码

#include<map>
#include<set>
#include<list>
#include<queue>
#include<deque>
#include<stack>
#include<ctime>
#include<cmath>
#include<vector>
#include<bitset>
#include<cstdio>
#include<cctype>
#include<string>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<iomanip>
#include<iostream>
#include<algorithm>
using namespace std;
#define reg register
template <typename T>
inline T read() {
    T a=0; char c=getchar(),f=1;
    while(c<'0'||c>'9') {
        if(c=='-') f=-f;
        if(c==-1) return c;
        c=getchar();
    }
    while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
    return a*f;
}
template <class T>
inline int write(T x) {
    if(x<0) x=(~x)+1, putchar('-');
    if(x/10) write(x/10);
    return putchar(x%10|48);
}
template <class T>
inline int write(T x,char c) {
    return write(x)&&putchar(c);
}
template <class T>
inline T Max(T a,T b) { return a>b?a:b; }
template <class T>
inline T Min(T a,T b) { return a<b?a:b; }
template <class T>
inline T Abs(T a) { return a<0?-a:a; }
int n=read<int>();
int C[500001];
long long ans;
int a[500001];
int rnk[500001];
inline int lowbit(int x) {
    return x & -x;
}
inline int query(int x) {
    int s=0;
    while(x>0) {
        s+=C[x];
        x-=lowbit(x);
    }
    return s;
}
inline void update(int x,int val) {
    while(x<=n) {
        C[x]+=val;
        x+=lowbit(x);
    }
}
int main() {
    for(reg int i=1;i<=n;i++) {
        a[i]=rnk[i]=read<int>();
    }
    sort(a+1,a+n+1);
    int cnt=unique(a+1,a+n+1)-a-1;
    for(reg int i=1;i<=n;i++) {
        rnk[i]=lower_bound(a+1,a+cnt+1,rnk[i])-a;
    }
    for(reg int i=1;i<=n;i++) {
        update(rnk[i],1);
        ans+=i-query(rnk[i]);
    }
    write(ans);
}

树状数组进阶

差分

如果你像我一样是个小白,那你也许看不懂这是什么东西。
好吧,先上万能的百度百科

差分,又名差分函数或差分运算,差分的结果反映了离散量之间的一种变化,是研究离散数学的一种工具,常用函数差近似导数。

有没有仍然看不懂?
那就直接来看一下差分的BIT吧

区间修改+单点查询

看起来有点正经的内容了

主要思想

令i~j的区间和为$a_i-a_{j-1}(i>j)$,于是前缀和就为$a_i-a_0$。如果如此,那输入的时候就可以处理为$update(i,a_i-a_{i-1})$。

单点查询

想一想,如果这样,$A_i=a_i-a_{i-1},A_1+A_2+...+A_i=a_i-a_0$,就可以直接这样处理了。至于单点查询,可以很容易地想到,对于一个点i而言,因为a[0]=0,所以query(i)=a[i]

区间修改

直接上代码

inline void update(int x,int delta) {
    ......//同上
}
inline void update(int l,int r,int delta) {
    update(l,delta); update(r+1,-delta);
}

想一想,如果$n=6$,$[2-4]$这个区间加上4,那么如下

id 1 2 3 4 5  6
A  0 4 4 4 0  0
C  0 4 0 0 -4 0

震惊! 只有$C_2(+4)$和$C_5(-4)$改变了!
对于$C_3$的分析

C[3]=A[3]-A[2]
C'[3]=A'[3]-A'[2]=(A[3]+4)-(A[2]+4)=A[3]-A[2]=C[3]

所以,按这样推来,只需要改动$C_l$和$C_r$即可。

区间修改+区间查询

差分分析

我们同样引入上面的差分C数组。
$∵C_i=A_i-A_{i-1}$
$∴A_i=C_1+C_2+C_3+......+C_i$
那么可以得到这一坨
$????A_1+A_2+...+A_i$
$=C_1+(C_1+C_2)+...+(C_1+C_2+...+C_i)$
$=iC_1+(i-1)C_2+...+C_i$
$=i(C_i+C_2+...+C_i)-1C_2-...-(i-1)C_i$
所以,我们就可以再用一个毒瘤的差分数组$C1_i$来储存$(i-1)
C_i$
接上面的公式
$=i*(C_1+C_2+C_3+......+C_i)-(C1_1+C1_2+C1_3+......+C1_i)$

代码

template <typename T>
inline T read() {
    T a=0; char c=getchar(),f=1;
    while(c<'0'||c>'9') {
        if(c=='-') f=-f;
        if(c==-1) return c;
        c=getchar();
    }
    while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
    return a*f;
}
typedef long long LL;
#define lowbit(x) (x & -x)
struct BIT{
    int n;
    LL C1[MAXN],C2[MAXN];
    inline BIT(){}
    inline void update(LL *C,int x,LL val) {
        while(x<=n) {
            C[x]+=val;
            x+=lowbit(x);
        }
    }
    inline void update(int l,int r,LL val) {
        update(C1,l,val); update(C1,r+1,-val);
        update(C2,l,val*(l-1)); update(C2,r+1,-val*r);
    }
    inline LL query(LL *C,int x) {
        LL s=0;
        while(x) {
            s+=C[x];
            x-=lowbit(x);
        }
        return s;
    }
    inline LL query(int l,int r) {
        return (l-1)*query(C1,l-1)-query(C2,l-1)-(r*query(C1,r)-query(C2,r));
    }
    inline void init(int N=0) {
        if(N) n=N;
        else n=read<int>();
        LL bef=0;
        for(reg int i=1;i<=n;i++) {
            LL num=read<LL>();
            update(C1,i,num-bef);
            update(C2,i,(num-bef)*(i-1));
            bef=num;
        }
    }
};

2D树状数组

怎么会有这么毒瘤的题……

int n,m;
struct BIT2D{
    int C[1001][1001];
    ......//函数
};

query

分析——放弃
代码——

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

update

分析——同上
代码——

inline void update(int x,int y,int delta) {
    for(register int i=x;i<=n;i+=lowbit(i)) {
        for(register int j=y;j<=n;j+=lowbit(j)) {
            C[i][j]+=delta;
        }
    }
}

区间修改+单点查询

一句话总结:$C_{i,j}=A_{i,j}-A_{i-1,j}-A_{i,j-1}+A_{i-1,j-1}$

区间修改+区间查询

同样是一句话:维护$C_{i,j}$和$iC_{i,j}$和$jC_{i,j}$和$ijC_{i,j}$

时间复杂度

每一次操作为$O(log^2)$

当然了,更高维的树状数组也可以此类推,每一次操作的时间复杂度为$O(log^k)$(k是树状数组的维度)

罗列例题

一维

完结撒花(图)!!!!!!!!!

---恢复内容结束---

@TOC


膜拜第

个来访本蒟蒻博客的大佬

beginning

让我们先从这道题开始:P1001 A+B Problem
说明: 我不是在搞笑 !
我们可以探讨一下,用我们所学的知识可以如何解决这道题.

顺序结构 A+B

#include<cstdio>
int a,b;
int main()
{
    scanf("%d%d",&a,&b);
    printf("%d",a+b);
}

明显没有任何毛病…… 傻子才会说有毛病

高精 A+B

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
    static const int LEN=100001;
    static const int ZR='0';
    int a[LEN];
    bigint() { a[0]=1,a[1]=0; }
    void operator=(char*s) {
        memset(a,0,sizeof(int)*a[0]);
        a[0]=S(s);
        for(int i=0;i<a[0];i++)
            a[a[0]-i]=s[i]-'0';
    }
    void operator=(int s) {
        char t[LEN];
        sprintf(t,"%d",s);
        *this=t;
    }
    bigint(int n) { *this=n; }
    bigint(char *s) { *this=s; }
    void in() {
        char s[LEN];
        scanf("%s",s);
        a[0]=S(s);
        for(int i=0;i<a[0];i++)
            a[a[0]-i]=s[i]-'0';
    }
    void out() {
        for(int i=a[0];i>=1;i--)
            printf("%d",a[i]);
    }
    bigint operator+(const bigint&s) {
        bigint ans;
        ans.a[0]=mx(a[0],s.a[0]);
        int x=0;
        for(int i=1;i<=ans.a[0];i++) {
            ans.a[i]=a[i]+s.a[i]+x;
            x=ans.a[i]/10;
            ans.a[i]%=10;
        }
        while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
        return ans;
    }
};
bigint a,b,c;
int main() {
    a.in();
    b.in();
    c=a+b;
    c.out();
}

压位高精 A+B

#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int S(const char *a) { return strlen(a); }
int mx(int a,int b) { return a>b?a:b; }
struct bigint{
    static const int LEN=101;
    int a[LEN];
    bigint(){}
    void operator=(char*s) {
        memset(a,0,sizeof(a));
        int len=S(s);
        a[0]=len/4;
        if(len%4) a[0]++;
        for(int i=0,j;i<len&&(j=(len-i-1)/4+1);i++)
            a[j]=a[j]*10+s[i]-'0';
    }
    void operator=(int s) {
        char t[LEN];
        sprintf(t,"%d",s);
        *this=t;
    }
    bigint(int n) { *this=n; }
    bigint(char *s) { *this=s; }
    void in() {
        char s[LEN];
        scanf("%s",s);
        *this=s;
    }
    void out() {
        printf("%d",a[a[0]]);
        for(int i=a[0]-1;i>=1;i--)
            printf("%04d",a[i]);
    }
    bigint operator+(const bigint&s) {
        bigint ans=0;
        ans.a[0]=mx(a[0],s.a[0]);
        for(int i=1;i<=ans.a[0];i++) {
            ans.a[i]+=a[i]+s.a[i];
            if(ans.a[i]>=10000)
                ans.a[i]-=10000,ans.a[i+1]++;
        }
        while(ans.a[ans.a[0]+1]!=0)ans.a[0]++;
        return ans;
    }
};
bigint a,b,c;
int main() {
    a.in();
    b.in();
    c=a+b;
    c.out();
}

二分A+B

#include<cstdio>
inline int read()
{
    int a=0; char f=1,c=getchar();
    while(c<'0'||c>'9')
    {
        if(c=='-') f=-f;
        if(c==-1) return -1;
        c=getchar();
    }
    while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
    return a*f;
}
inline int write(int a)
{
    if(a<0) a=(~a)+1;
    if(a/10) write(a/10);
    return putchar(a%10|48);
}
int a=read(),b=read(),l=-2e9,r=2e9;
int main()
{
    while(l+1<r)
    {
        int mid=(l+r)>>1;
        if(mid<a+b) l=mid;
        else r=mid;
    }
    write(r);
}

或许很多人已经觉得二分A+B已经很神奇了,但是我们今天要学习一种新的做A+B的方法:树状数组(B.I.T).

树状数组简介(不喜欢啰嗦的请直接跳到这里)

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

经过了如此一番看不懂的说明,或许你会直接绝望掉,But这东西贼重要,而且 这种东西竟然没有STL!!!气不气 QAQ

基础概念

假设数组$A_{1...n}$,那么查询$\sum_{j=1}^{i}A_j$的时间是$log$级别的,而且这是一个在线的数据结构,支持随时修改某个元素的值,复杂度也为$log$级别。

来观察这个图:
令这棵树的结点编号为$C_1,C_2,C_3......C_n$。令每个结点的值为这棵树的值的总和,那么容易发现:

C1 = A1
C2 = A1 + A2
C3 = A3
C4 = A1 + A2 + A3 + A4
C5 = A5
C6 = A5 + A6
C7 = A7
C8 = A1 + A2 + A3 + A4 + A5 + A6 + A7 + A8

这里有一个有趣的性质:
设节点编号为x,那么这个节点管辖的区间为$2^k$(其中k为x二进制末尾0的个数)个元素。

这样说是不是就要明确一些了?

根据这个性质,就有一个千古大罪人发明了树状数组

代码实现

大体结构

struct B_I_T{
    int C[100001];
    int n;
    ......//函数
};

lowbit

时间复杂度

$O(1)$

实现

还记得上面说的那一个性质吗?借助C++强大的位运算,我们可以在O(1)时间内求出$2^k$

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

还有一种更简单也更常用的方式,是这样的

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

struct B_I_T{
    int C[100001];
    int n;
    inline int lowbit(int x) { return x & -x; }
    ......//函数
};

就可以在结构体中加入这样一个函数了.
不过,更常用的方法不是写在结构体里面,而是写在外面,否则不在结构体当中就只能用B_I_T::lowbit(x) 了,即

inline int lowbit(int x) { return x & -x; }
struct B_I_T{
    int C[100001];
    int n;
    ......//函数
};

其实还有一种实现方法,用宏定义:#define lowbit(x) (x & -x)

lowbit的作用

update

时间复杂度

$O(log n)$

意义

$update(x,val)$ => $A_x=A_x+val$

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

这种使用for循环的做法,和下面使用while循环的原理是一样的.

void update(int x,int val) {
    while(x<=n) {
        C[x]+=val;
        x+=lowbit(x);
    }
}

假设n=8,执行update(3,5),则有如下流程

x=3 C[3]+=5 x=3+lowbit(3)=4
x=4 C[4]+=5 x=4+lowbit(4)=8
x=8 C[8]+=5 x=8+lowbit(8)=16
x=16 退出

对照上文的图片 我也不知道有多上文 我们可以知道,每一次C数组中执行加操作的下标,刚好都包括了$A_3$!

作用

作用1——初始化

inline void init() {
    scanf("%d",&n);
    for(reg int i=1;i<=n;i++) {
        scanf("%d",&a);
        update(i,a);
    }
}

作用2——改变单点的值 话说就是拿来干这件事的就不讲了

query

时间复杂度

$O(log n)$

意义

$query(x)$ => $A_1+A_2+……+A_x$

实现
int query(int x) {
    int res=0;
    for(reg int i=x;i;i-=lowbit(x)) res+=C[i];
    return res;
}

这种使用for循环的做法,和下面使用while循环的原理是一样的.

int query(int x) {
    int res=0;
    while(x) {
        res+=C[x];
        x-=lowbit(x);
    }
    return res;
}

假设n=8,执行query(7),则有如下流程

res=0
x=7 res=C[7]
x=6 res=C[6]+C[7]
x=4 res=C[4]+C[6]+C[7]
x=0 退出

仍然对照上(?)表,可以知道$C_4=A_1+A_2+A_3+A_4$,$C_6=A_5+A_6$,$C_7=A_7$。所以$C_4+C_6+C_7=\sum^7_{i=1}A_i$!

作用

作用1——求单点前缀和 不多赘述,直接$query(x)$即可
作用2——求区间和 $Sum(l,r)=query(r)-query(l-1)$,即r的前缀和减去l-1的前缀和,即为l->r的区间和

总结+代码

将以上的所有总结在一起,可以有如下代码

struct B_I_T{
    int n;
    int C[MAXN];
    inline int lowbit(int x) {
        return x & -x;
    }
    inline void update(int x,int val) {
        for(register int i=x;i<=n;i+=lowbit(i))
            C[i]+=val;
    }
    inline int query(int x) {
        int s=0;
        for(register int i=x;i;i-=lowbit(i))
            s+=C[i];
        return s;
    }
    inline void init() {
        for(register int i=1;i<=n;i++) {
            int a;
            scanf("%d",&a);
            update(i,a);
        }
    }
};

如此,我们就可以愉快地完成开头的问题A+B problem了!

逆序对

话说这不一道归并的题吗?拿到B.I.T.这里来干哈的?
先无脑打上mergesort的代码一份

int n,a[500001];
int s[500001];
long long ans;
void mrg(int left,int right,int mid)
{
    int i=left,j=mid+1,k=left;
    while(i<=mid&&j<=right)
    {
        if(a[i]>a[j])
        {
            ans+=mid-i+1;
            s[k++]=a[j++];
        }
        else s[k++]=a[i++];
    }
    while(i<=mid)
        s[k++]=a[i++];
    while(j<=right)
        s[k++]=a[j++];
    for(int i=left;i<=right;i++)
        a[i]=s[i];
}
void mergesort(int left,int right)
{
    if(left==right)return;
    int mid=(left+right)/2;
    mergesort(left,mid);
    mergesort(mid+1,right);
    mrg(left,right,mid);
}

离散化

喂喂喂,这又是什么鬼?又关这道题什么事?不要急,先听我说。

离散化是程序设计中一个常用的技巧,它可以有效的降低时间复杂度。
有些数据本身很大,自身无法作为数组的下标保存对应的属性。如果这时只是需要这堆数据的相对属性,那么可以对其进行离散化处理。当数据只与它们之间的相对大小有关,而与具体是多少无关时,可以进行离散化。比如当你数据个数n很小,数据范围却很大时(超过1e9)就考虑离散化更小的值,能够实现更多的算法。

Set an example:

方式

离散化常见的两种方式: 1、数组离散化 2、用STL+二分离散化

数组法

for(register int i=1;i<=n;i++)
{
    cin>>a[i].val;
    a[i].id = i;
}
sort(a+1,a+n+1);//定义结构体时按val从小到大重载
for(int i=1;i<=n;i++)
    b[a[i].id]=i;//将a[i]数组映射成更小的值,b[i]就是a[i]对应的rank(顺序)值

STL+二分

//n原数组大小   num原数组中的元素    lsh离散化的数组    cnt离散化后的数组大小
int lsh[MAXN],cnt,num[MAXN],n;
for(int i=1;i<=n;i++)
{
    scanf("%d",&num[i]);
    lsh[i]=num[i];//复制一份原数组
}
sort(lsh+1,lsh+n+1);//排序,unique虽有排序功能,但交叉数据排序不支持,所以先排序防止交叉数据
//cnt就是排序去重之后的长度
cnt=unique(lsh+1,lsh+n+1)-lsh-1;//unique返回去重之后最后一位后一位地址-数组首地址-1
for(int i=1;i<=n;i++)
    num[i]=lower_bound(lsh+1,lsh+cnt+1,num[i])-lsh;
//lower_bound返回二分查找在去重排序数组中第一个等于或大于num[i]的值的地址-数组首地址,从而实现离散化

实现

逆序对实际上就是统计当前元素的前面有几个比它大的元素的个数,然后把所有元素比它大的元素总数垒加就是逆序对总数。


代码

#include<map>
#include<set>
#include<list>
#include<queue>
#include<deque>
#include<stack>
#include<ctime>
#include<cmath>
#include<vector>
#include<bitset>
#include<cstdio>
#include<cctype>
#include<string>
#include<cstdlib>
#include<cstring>
#include<climits>
#include<iomanip>
#include<iostream>
#include<algorithm>
using namespace std;
#define reg register
template <typename T>
inline T read() {
    T a=0; char c=getchar(),f=1;
    while(c<'0'||c>'9') {
        if(c=='-') f=-f;
        if(c==-1) return c;
        c=getchar();
    }
    while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
    return a*f;
}
template <class T>
inline int write(T x) {
    if(x<0) x=(~x)+1, putchar('-');
    if(x/10) write(x/10);
    return putchar(x%10|48);
}
template <class T>
inline int write(T x,char c) {
    return write(x)&&putchar(c);
}
template <class T>
inline T Max(T a,T b) { return a>b?a:b; }
template <class T>
inline T Min(T a,T b) { return a<b?a:b; }
template <class T>
inline T Abs(T a) { return a<0?-a:a; }
int n=read<int>();
int C[500001];
long long ans;
int a[500001];
int rnk[500001];
inline int lowbit(int x) {
    return x & -x;
}
inline int query(int x) {
    int s=0;
    while(x>0) {
        s+=C[x];
        x-=lowbit(x);
    }
    return s;
}
inline void update(int x,int val) {
    while(x<=n) {
        C[x]+=val;
        x+=lowbit(x);
    }
}
int main() {
    for(reg int i=1;i<=n;i++) {
        a[i]=rnk[i]=read<int>();
    }
    sort(a+1,a+n+1);
    int cnt=unique(a+1,a+n+1)-a-1;
    for(reg int i=1;i<=n;i++) {
        rnk[i]=lower_bound(a+1,a+cnt+1,rnk[i])-a;
    }
    for(reg int i=1;i<=n;i++) {
        update(rnk[i],1);
        ans+=i-query(rnk[i]);
    }
    write(ans);
}

树状数组进阶

差分

如果你像我一样是个小白,那你也许看不懂这是什么东西。
好吧,先上万能的百度百科

差分,又名差分函数或差分运算,差分的结果反映了离散量之间的一种变化,是研究离散数学的一种工具,常用函数差近似导数。

有没有仍然看不懂?
那就直接来看一下差分的BIT吧

区间修改+单点查询

看起来有点正经的内容了

主要思想

令i~j的区间和为$a_i-a_{j-1}(i>j)$,于是前缀和就为$a_i-a_0$。如果如此,那输入的时候就可以处理为$update(i,a_i-a_{i-1})$。

单点查询

想一想,如果这样,$A_i=a_i-a_{i-1},A_1+A_2+...+A_i=a_i-a_0$,就可以直接这样处理了。至于单点查询,可以很容易地想到,对于一个点i而言,因为a[0]=0,所以query(i)=a[i]

区间修改

直接上代码

inline void update(int x,int delta) {
    ......//同上
}
inline void update(int l,int r,int delta) {
    update(l,delta); update(r+1,-delta);
}

想一想,如果$n=6$,$[2-4]$这个区间加上4,那么如下

id 1 2 3 4 5  6
A  0 4 4 4 0  0
C  0 4 0 0 -4 0

震惊! 只有$C_2(+4)$和$C_5(-4)$改变了!
对于$C_3$的分析

C[3]=A[3]-A[2]
C'[3]=A'[3]-A'[2]=(A[3]+4)-(A[2]+4)=A[3]-A[2]=C[3]

所以,按这样推来,只需要改动$C_l$和$C_r$即可。

区间修改+区间查询

差分分析

我们同样引入上面的差分C数组。
$∵C_i=A_i-A_{i-1}$
$∴A_i=C_1+C_2+C_3+......+C_i$
那么可以得到这一坨
$????A_1+A_2+...+A_i$
$=C_1+(C_1+C_2)+...+(C_1+C_2+...+C_i)$
$=iC_1+(i-1)C_2+...+C_i$
$=i(C_i+C_2+...+C_i)-1C_2-...-(i-1)C_i$
所以,我们就可以再用一个毒瘤的差分数组$C1_i$来储存$(i-1)
C_i$
接上面的公式
$=i*(C_1+C_2+C_3+......+C_i)-(C1_1+C1_2+C1_3+......+C1_i)$

代码

template <typename T>
inline T read() {
    T a=0; char c=getchar(),f=1;
    while(c<'0'||c>'9') {
        if(c=='-') f=-f;
        if(c==-1) return c;
        c=getchar();
    }
    while(c>='0'&&c<='9') a=(a<<1)+(a<<3)+(c^48),c=getchar();
    return a*f;
}
typedef long long LL;
#define lowbit(x) (x & -x)
struct BIT{
    int n;
    LL C1[MAXN],C2[MAXN];
    inline BIT(){}
    inline void update(LL *C,int x,LL val) {
        while(x<=n) {
            C[x]+=val;
            x+=lowbit(x);
        }
    }
    inline void update(int l,int r,LL val) {
        update(C1,l,val); update(C1,r+1,-val);
        update(C2,l,val*(l-1)); update(C2,r+1,-val*r);
    }
    inline LL query(LL *C,int x) {
        LL s=0;
        while(x) {
            s+=C[x];
            x-=lowbit(x);
        }
        return s;
    }
    inline LL query(int l,int r) {
        return (l-1)*query(C1,l-1)-query(C2,l-1)-(r*query(C1,r)-query(C2,r));
    }
    inline void init(int N=0) {
        if(N) n=N;
        else n=read<int>();
        LL bef=0;
        for(reg int i=1;i<=n;i++) {
            LL num=read<LL>();
            update(C1,i,num-bef);
            update(C2,i,(num-bef)*(i-1));
            bef=num;
        }
    }
};

2D树状数组

怎么会有这么毒瘤的题……

int n,m;
struct BIT2D{
    int C[1001][1001];
    ......//函数
};

query

分析——放弃
代码——

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

update

分析——同上
代码——

inline void update(int x,int y,int delta) {
    for(register int i=x;i<=n;i+=lowbit(i)) {
        for(register int j=y;j<=n;j+=lowbit(j)) {
            C[i][j]+=delta;
        }
    }
}

区间修改+单点查询

一句话总结:$C_{i,j}=A_{i,j}-A_{i-1,j}-A_{i,j-1}+A_{i-1,j-1}$

区间修改+区间查询

同样是一句话:维护$C_{i,j}$和$iC_{i,j}$和$jC_{i,j}$和$ijC_{i,j}$

时间复杂度

每一次操作为$O(log^2)$

当然了,更高维的树状数组也可以此类推,每一次操作的时间复杂度为$O(log^k)$(k是树状数组的维度)

罗列例题

一维

完结撒花(图)!!!!!!!!!

原文地址:https://www.cnblogs.com/PI-UKE/p/10490794.html

时间: 2024-10-10 07:06:59

浅析树状数组的相关文章

浅析树状数组(二叉索引树)及一些模板

树状数组 动态连续和查询问题.给定一个n个元素的数组a1.a2.……,an,设计一个数据结构,支持以下两种操作:1.add(x,d):让ax增加d;2.query(l,r):计算al+al+1+…+ar 如何让query和add都能快速完成呢?方法有很多,这里介绍的便是树状数组.为此我们先介绍lowbit. 对于正整数x,我们定义lowbit(x)为x的二进制表达式中最右边的1所对应的值(而不是这个比特的序号).比如,38288的二进制1001010110010000,所以lowbit(3828

HDU 5542 The Battle of Chibi dp+树状数组

题目:http://acm.hdu.edu.cn/showproblem.php?pid=5542 题意:给你n个数,求其中上升子序列长度为m的个数 可以考虑用dp[i][j]表示以a[i]结尾的长度为j的上升子序列有多少 裸的dp是o(n2m) 所以需要优化 我们可以发现dp的第3维是找比它小的数,那么就可以用树状数组来找 这样就可以降低复杂度 #include<iostream> #include<cstdio> #include<cstring> #include

(POJ 3067) Japan (慢慢熟悉的树状数组)

Japan Time Limit: 1000MS   Memory Limit: 65536K Total Submissions: 29295   Accepted: 7902 Description Japan plans to welcome the ACM ICPC World Finals and a lot of roads must be built for the venue. Japan is tall island with N cities on the East coas

【二维树状数组】See you~

https://www.bnuoj.com/v3/contest_show.php?cid=9148#problem/F [题意] 给定一个矩阵,每个格子的初始值为1.现在可以对矩阵有四种操作: A x y n1 :给格点(x,y)的值加n1 D x y n1: 给格点(x,y)的值减n1,如果现在格点的值不够n1,把格点置0 M x1 y1 x2 y2:(x1,y1)移动给(x2,y2)n1个 S x1 y1 x2 y2 查询子矩阵的和 [思路] 当然是二维树状数组 但是一定要注意:lowbi

Vijos P1066 弱弱的战壕【多解,线段树,暴力,树状数组】

弱弱的战壕 描述 永恒和mx正在玩一个即时战略游戏,名字嘛~~~~~~恕本人记性不好,忘了-_-b. mx在他的基地附近建立了n个战壕,每个战壕都是一个独立的作战单位,射程可以达到无限(“mx不赢定了?!?”永恒[email protected][email protected]). 但是,战壕有一个弱点,就是只能攻击它的左下方,说白了就是横纵坐标都不大于它的点(mx:“我的战壕为什么这么菜”ToT).这样,永恒就可以从别的地方进攻摧毁战壕,从而消灭mx的部队. 战壕都有一个保护范围,同它的攻击

CF 313 DIV2 B 树状数组

http://codeforces.com/contest/313/problem/B 题目大意 给一个区间,问你这个区间里面有几个连续相同的字符. 思路: 表示个人用树状数组来写的...了解了树状数组的本质就行了. 当然用sum[r]-sum[l]也是可以的

Hdu5032 极角排序+树状数组

题目链接 思路:参考了题解.对询问进行极角排序,然后用树状数组维护一下前缀和即可. /* ID: onlyazh1 LANG: C++ TASK: test */ #include<bits/stdc++.h> using namespace std; #define lson l,m,rt<<1 #define rson m+1,r,rt<<1|1 typedef long long ll; const int maxn=1010; const int maxm=10

Curious Robin Hood(树状数组+线段树)

1112 - Curious Robin Hood    PDF (English) Statistics Forum Time Limit: 1 second(s) Memory Limit: 64 MB Robin Hood likes to loot rich people since he helps the poor people with this money. Instead of keeping all the money together he does another tri

【初识——树状数组】 区间求最值

说树状数组其实是一个索引表,但是是一个特殊的,树状的索引表,它利用了二进制的一些特性. 就区间求和的要求来说: 首先我们用a[]数组来存储原始数据.然后在a[]之上构造c[]数组来作为树状数组. 如图 这个图表示,当i为奇数时,c[i]中保存的都是a[i]本身.然后,c[2]中保存了a[1], a[2],共2个,c[4]中保存的是a[1], a[2], a[3], a[4],c[6]又是保存两个,c[5]和c[6].c[8]保存8个,c[1], c[2], c[3], c[4], c[5], c