树状数组————(神奇的区间操作)蒟蒻都可以看懂,因为博主就是个蒟蒻

树状数组

F三、蒟蒻看法

树状数组是十分神奇的算法(至少我这么认为,我至今都好奇,第一个人是如何想出来的,oi深似海比海深比海深)。

F二、树状数组存在的含义(要它干啥子)用途!用途!

其实标题已经说明了,它可以支持一些区间操作,比如最基础的单点修改、区间查询,区间修改,单点查询,和一些精巧的应用。(哦~~,我的数组也可以实现)那你就等着T吧。树状数组的速度是真的快,比线段树都要快!但是理解起来有一点点麻烦,所以有了这篇博客。

F一、什么是树状数组

与它的名字一样,就是用数组来实现树形结构,是不是很腻害!而且代码短!代码短!

一、初识树状数组(介绍)

对,他就是这样的,红色的代表我们今天要学的树状数组,而黑色便是普通的数组,这张图代表了他们的对应关系。(??what?What are you say?)

不要急,咱们先来点基础知识。

二、前置知识(lowbit可是树状数组的灵魂)

我们都知道,任何一个数都可以用2不同的幂相加得到:

x=2i1+2i2+2i3+2i4+……+2i+n,其中i1>i2>i3>i4>……>im,例如:7=22+21+20=4+2+1.

这也就意味着一段区间可以划分为大小不同的段数,例如:[1,7]=[1,4]+[5,6]+[7,7].(上面的例子数的大小与这个例子的区间长度一一对应)。

上面的图不好,咱们再来一张。(不慌不慌)

 

同样,最底下为输入的数(也就是数组,这里设为a[i]),上方对印的便是树状数组。

咱们还拿7为例子,[1,7]=[1,4]+[5,6]+[7,7]同等与a[1]+a[2]+a[3]+a[4]+a[5]+a[6]+a[7]=c[4]+c[6]+c[7].

但这是如何实现的呢?答:lowbit(n).

又要前置知识了,lowbit可以求出一个数在二进制数中最低位1以其他后边的0所构成的值。理解的人请跳过本段。首先是二进制数,二进制讲解:咱们平时的运算都是十进制的(即逢十进一),二进制便是逢二进一。为了实现lowbit(n)操作,我们将n取反变为~n(取反就是,将原本为1的变为0,为0的变为1),这样原本n的最后一位1的位置(设为k)~n的第k位为0,因为原本n的最后一位1的位置为k,所以k以后全是0,所有~n第k位以后全是1,此时我们将~n+1(此时,~n第k位以后的1都会因为进位变成0,而第k位因为进位变成了1),这时,n与~n只有第k位都是1(即n的最低位的1),其他位置数都不相等,再用n&(~n+1)(&如果此位置都是1,返回1,否则返回0),所以返回了n最低位1以其他后边的0所构成的值。在补码(取反加以)的表示下,~n=-1-n,所以:n=~n+1,因此:lowbit(n)=n&(~n+1)=n&(-n).             好,lowbit()讲解结束,别看难证明,计算机里此种运算飞快(因为计算机本身就是二进制的)。

(so~  lowbit与树状数组有what关系)

 

三、区间查询

再看上面的图,找找关系。(哇!没找见)很好,举个例子,还是7,咱们把7取lowbit,lowbit(7)=1,lowbit(7-1)=2,lowbit(7-1-2)=4,有没有什么发现,没错,它一直取lowbit可以得到它每段的长度(没个啥子用呀),再看,7,7-1=6,7-1-2=4,c[7]+c[6]+c[4]=结果。(这回总算有用了)

上代码!

查询前缀和(没错,你已经掌握了树状数组支持的基本操作之一————查询前缀和

inline int ask(int x) {//x的前缀和
    int ans=0;
    for(; x; x -= x & -x) ans+=c[x];
    return ans;
}

那为啥要查询前缀和呢?照常举例子,比如咱们要查询[l,r]中所有数的和,咱们只需要计算ask(r)-ask(l-1)。

四、单点修改

 树状数组支持的第二个操作是单点修改(此处运用单点增加,变化不大)。

咱们再看看lowbit与上面的图,很好,你还是看不出来,举个例子,还是7,咱们把7取lowbit,lowbit(7)=1,lowbit(7+1)=8,lowbit(7+1+2)=16,再看,7,7+1=8,7+1+8=16,c[16]包括c[8],c[8]包括c[7]。

当咱们修改了a[x](假设将a[x]加上b),咱们同时也要将它的祖先(包含它的c[])全部修改,这样才能支持查询前缀和的操作。

上代码!

单点增加

inline void add(int x,int b) {//将序列中一个数a[x]及其祖先加上b;
    for(; x<=n; x += x & -x) c[x]+=b;
}

没错你学会了,树状数组!

值得注意的是,当你输入原数组时记得也要执行add(x,a[x]).

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

五、支持单点修改、区间查询的树状数组全貌

来!欣赏(姑且瞎看)一下代码的全貌吧!

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

using namespace std;

int n,m;
int a[500008],c[500008];

inline void add(int x,int b) {
    for(; x<=n; x += x & -x) c[x]+=b;
}

inline int ask(int x) {
    int ans=0;
    for(; x; x -= x & -x) ans+=c[x];
    return ans;
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) {
        cin>>a[i];
        add(i,a[i]);
    }
    while(m--) {
        int p,x,k;
        cin>>p>>x>>k;
        if(p==1) add(x,k);
        else cout<<ask(k)-ask(x-1)<<endl;
    }
}

六、支持区间修改、单点查询的树状数组

先来区间修改,不知你是否有听过差分。差分是一种很常见又很神奇的算法,它可以通过改变两个点的值,使整段区间发生修改。

差分讲解:假设a[]为原数组,b[]为差分数组

则:b[i]=a[i]-a[i-1]

b[1]=a[1]-a[0]                  b[2]=a[2]-a[1]                b[3]=a[3]-a[2]   ……    b[n]=a[n]-a[n-1]

若将a[i]到a[j](i<j)同时加x,则b[i]以前并未发生改变,b[i]=b[i]+x,b[i+1]到b[j](包括b[i+1]和b[j])之间并未发生改变,b[j+1]=b[j+1]-x.

举个例子,将a[3]到a[7]同时加上x,因为b[3]以前的数字并不由a[3]到a[7]之间的数字得到,由于a[3]=a[3]+x,而a[2]=a[2],所以b[3](现在的)=(a[3]+x)-a[2]=a[3]-a[2]+x,b[3](原来的)=a[3]-a[2],所以b[3](现在的)=b[3](原来的)+x.因为a[3]到a[7]所有数字都加上了x所以b[4]到b[7]之间的数并没有发生变化,例子:b[4](现在的)=(a[4]+x)-(a[3]+x)=a[4]-a[3],b[4](原来的)=a[4]-a[3],所以b[4](现在的)=b[4](原来的).可是到了b[8]的时候,a[7]=a[7]+x,而a[8]=a[8],所以b[8](现在的)=a[8]-(a[7]+x)=a[8]-a[7]-x,b[8](原来的)=a[8]-a[7],b[8](现在的)=b[8](原来的)-x.证毕。

这样我们就可以通过改变a[i]和a[j+1]的大小,将a[i]到a[j]全部改变了。

add函数并未发生变化

add函数

inline void add(int x,int y) {
    for(;x<=n;x+=(x&-x)) c[x]+=y;
}

主函数有一些改变

add(a,c);add(b+1,-c);

通过改变两个点来改变整段添加操作。

(有啥子用?搞一堆乱七八糟的。)

再来单点查询

它是很有用的,差分还有一个性质:差分与前缀和(前面所有数字相加的和)互逆。意思就是数组差分后求个前缀和等于原数组。

是不是恍然顿悟!(查询a[k],就查询a[k]差分完后的前缀和)

没错,之前(支持单点修改、区间查询的树状数组)的查询方式就是查前缀和,所以查询代码还不用修改,甚至主程序里相关内容更简单了。

ask函数

inline int ask(int x) {
    int ans=0;
    for(;x;x-=(x&-x)) ans+=c[x];
    return ans;
}

主程序

cout<<ask(x)<<endl;

七、支持区间修改、单点查询的树状数组代码全貌

注意在输入原数组时记得也要用差分

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

好了上全部代码!

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

using namespace std;

int n,m;
int a[500008],c[500008];

inline void add(int x,int y) {
    for(;x<=n;x+=(x&-x)) c[x]+=y;
}

inline int ask(int x) {
    int ans=0;
    for(;x;x-=(x&-x)) ans+=c[x];
    return ans;
}

int main()
{
    cin>>n>>m;
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
        add(i,a[i]-a[i-1]);
    }
    for(int i=1;i<=m;i++) {
        int p;
        cin>>p;
        if(p==1) {
            int a,b,c;
            cin>>a>>b>>c;
            add(a,c);add(b+1,-c);
        }
        else {
            int x;
            cin>>x;
            cout<<ask(x)<<endl;
        }
    }
}

(一堆骚操作,乱七八糟,跟直接用数组解决有区别吗?)

显然是有的。

七、复杂度正儿八经瞎七八证明

首先是支持单点修改、区间查询的树状数组,虽然修改的速度变慢了,从O(1)变成了O(log(n)),但它为查询操作提供了方便,使查询从O(n)变成了O(log(n)).

其次是支持区间修改、单点查询的树状数组,虽然查询的速度变慢了,从O(1)变成了O(log(n)),但它为修改操作提供了方便,使查询从O(n)变成了O(log(n)).

(没看出我是复制的吧!qwq)

八、树状数组解决逆序对

很好,在博主奋力的解说下(可能压根没人看到这里吧),和严谨的复杂度证明后,相信你已经对树状数组的基操有了很深的认识。

一起看一道,树状数组模版题吧!

输入样例:

6
5 4 2 6 3 1

输出样例:

11

是不是有些许蒙蔽。

我先说明一下逆序对:逆序对就是如果i > j && a[i] < a[j],这两个就算一对逆序对,简单来说,所有逆序对的个数和就是找每一个数的前面有几个比他的大的数,他们加起来的和就是逆序对的总数

(这也能用树状数组?)

事实证明是可以的。

我要开始讲解了!(先自我思考一下!)

首先,我们将数b[i]输入,然后每次都把c[b[i]]++,查询c[b[i]]的后缀和(顾名思义,就是后面全部数字的和,但此处的后缀和不包括自己),此处的后缀和代表的就是在b[i]之前输入但大于b[i]的数(即:符合i > j && a[i] < a[j])个数之和,也就是逆序对的个数。

例子来惹!

第一次把5的位置加1,它的后缀和(以下全为不包括自己的后缀和)为0,sum+=0(sum记录逆序对个数)

第二次把4的位置加1,它的后缀和为1,sum+=1

 

第三次把2的位置加1,它的后缀和为2,sum+=2

第四次把6的位置加1,它的后缀和为0,sum+=0

第五次把3的位置加1,它的后缀和为3,sum+=3

第五次把1的位置加1,它的后缀和为5,sum+=5

此时sum=11,输出sum。

(哇!明显炸了呀!序列数字不超过109呀,怎么存?)

没错!没错!而且很明显还没用到树状数组。那它到底如何实现的呢?

首先要离散化(有链接)!(因为需要用到桶的思想)

虽然每个数的值很大,但是(n<=5*105)却是可以开下的,离散化可以保证数字间的相对大小不变,而本题恰恰只需要这点。

代码!代码!

for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
        A[i]=a[i];
}
    sort(a+1,a+n+1);
    int size=unique(a+1,a+n+1)-a;
for(int i=1;i<=n;i++) {
        b[i]=lower_bound(a+1,a+size+1,A[i])-a;
}

实现了把a[](输入的数组),离散化成b[]。

接下来就是如何和树状数组联系了。

通过上述讲解,我们将问题简化为了,每次在c[b[i]]++,查询后缀和。(这不是树状数组所支持的单点修改区间查询吗?)

先看单点修改。

同上文一样在b[i]的位置上加上1。

for(int i=1;i<=n;i++) {
    add(b[i],1);//添加操作
    //sum+=i-ask(b[i]);//查询操作
}

 

(但是后缀和怎么办?我们求的是前缀和呀?)

i-前缀和=不包括自己的后缀和(因为i代表这是第几个数,而前缀和是1~i(因为每回只加1))

所以就解决了!

for(int i=1;i<=n;i++) {
    add(b[i],1);
    sum+=i-ask(b[i]);
}

此题解将输入的数存在a[],将离散化的数存在b[](就是之前树状数组模版中的普通数组),树状数组用c[]。

代码全貌!

//用树状数组实现查询逆序对个数 
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>

using namespace std;

int n,a[500005],A[500005],b[500005],c[500005];
long long sum;

inline void add(int x,int b) {
    for(; x<=n; x += x & -x) c[x]+=b;
}

inline int ask(int x) {
    int ans=0;
    for(; x; x -= x & -x) ans+=c[x];
    return ans;
}

int main()
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++) {
        scanf("%d",&a[i]);
        A[i]=a[i];
    }
    sort(a+1,a+n+1);
    int size=unique(a+1,a+n+1)-a;
    for(int i=1;i<=n;i++) {
        b[i]=lower_bound(a+1,a+size+1,A[i])-a;
    }
    for(int i=1;i<=n;i++) {
        add(b[i],1);
        sum+=i-ask(b[i]);
    }
    printf("%lld",sum);
}

某谷链接!

记得开龙龙(long long)啊!不开龙龙见祖宗!

没开龙龙

开了龙龙

博主语录:

1、树状数组是真的神奇!不明白怎么想出来的。

2、树状数组虽然比较难理解,但是速度,和代码难度是真的优秀!建议勤加练习。

3、若是没能理解可以先看看线段树,几乎oi界的人都是先理解了线段树的。(可能是我太菜,目光短浅)

 

 

posted @ 2019-07-15 20:32  沉~杉  阅读(363)  评论(0编辑  收藏  举报
Live2D