树状数组自学笔记

树状数组自学笔记

树状数组和线段树都是查询\(O(logn)\)的数据结构。

但是为什么很多人宁愿用树状数组而不是用线段树呢?

因为树状数组写起来比线段树在一定程度上简单多了。(Author:理解看了这篇文章也OK)

但是!树状数组维护的数据局限性要比线段树要大——这验证了一句话:

越复杂的数据结构时间复杂度越小,但是越暴力的算法全能性越大。

真理......

Author:好我们先不扯淡了

先来上一个例子:

如果给你一个数列(n个数),然后让你求出区间\([l,r]\)的和。

分类讨论一下n:

\(n\leq1000\),很明显,暴力。

\(n\leq 10000\)继续吸氧暴力......

\(n \leq 10^6\)......线段树?

但是,作为蒟蒻,我表示线段树RE好几次了......

咋整啊......

这时候,一种新的算法腾空出世:前缀和!

一、树状数组基础1——前缀和

前缀和,采取动态规划思想,通项公式:

\(n[i]=n[i-1]+a[i]\)

然后有点数学常识就可以看得出来\(O(1)\)访问区间和(??详见下注)。

Tip:第n项+之前的和就是$a[n]$,询问第n项即$a[n]-a[n-1]$


以上只不过是思想基础1,下面才是真正扯的树状数组。

二、树状数组原理

1.我们先来观察这样一个图:

timg?image&quality=80&size=b9999_10000&sec=1532683198527&di=23ee4a3653ed357d2cb3246297cf3833&imgtype=jpg&src=http%3A%2F%2Fimg4.imgtn.bdimg.com%2Fit%2Fu%3D970540049%2C2575012713%26fm%3D214%26gp%3D0.jpg

蓝的是左儿子,红的是右儿子(废话)

然后看看线段树,我们发现线段树的节点是有重叠部分的——父节点各种重叠,导致空间存储各种爆炸。

如果我们去掉这个烦人的重叠,发现好像空间复杂度降低了,时间复杂度不变。

空间到底是减少了多少呢?

手算一下:

最上面是减少了\(1\over 2\)

然后是\(1 \over 4\)

\(1\over 8\)......

空间竟然一步步变成了O(n)......

2.但是怎么访问呢???

神奇的地方来了:

再看一个图——

u=3976234040,1784782209&fm=15&gp=0.jpg

我们发现凡是\(2^n\)都是一群单个1的,然后是,然后是......

恍然大悟——

原来我们只需要前缀和一样的存储方式

前缀和一样的访问方式

(戴望舒:OI一样的凄婉迷茫)

然后我们要加上或者减去一个数的二进制中所在的位2^二进制中1所在的位

——这时候再引入一个东西

3.lowbit

求最低位权。

num的位权就是num最低位的'1'和左边的'0'(如果有的话)组成的数字
(Tip:位权不一定是二进制)

根据计算机补码(位运算玄学操作),lowbit(n)=n & -n

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

于是我们得到如下操作:

很明显:\(C_n = A_{(n – 2^k + 1)} + ... + A_n\)
相对于维护,这样的计算省下多少力气...

我们使用一个while循环来实现根节点到子节点的循环。

使用树状数组只需要反复进行某些步骤就好了:
  • 1.初始化sum=0;
  • 2.如果\(n\leq 0\),停止;否则就\(sum+=c[n]\);
  • 3.\(n-=lowbit(n);\)然后是第二步。
int ask_(int k)//前k个数的和 
{
    int ans=0;//初始化ans=0
    while(k>0)//不超过左界
    {
        ans+=t[k];//加上左边所有节点的数字
        k-=lowbit(k);
    }
    return ans;
}
int ask_seg(int l,int r)
{
    return ask_(r)-ask_(l);
}

以上代码是求区间[1,k]的方式。

Tip:\(\sum_{i=x}^y[x,y]\)等同于求$(\sum_{i=1}^yarray[i],i\in[1,y])-(\sum_{j=1}^x array[j],j\in[1,x]) $。

如果是单点修改呢?
void change_p(int pos,int num)
{
    while(i<=n)//0.在数组内部
    {
        c[i]=c[i]+x;//1.修改
        i+=lowbit(i);//2.跳转到下一个与其有关的值
    }
}

现在又面临一个严峻的问题:

区间修改咋整啊?

我们还是引进新的概念——

三、树状数组基础2——数列差分

差分简直是一个神奇的东西。。。

首先我们介绍这个还得引入一个问题:

如何快速地改变一个数列的值?朴素算法

常数大了呢?

差分!

差分数组定义:

我们求出一个类似dp求差分数组的公式:
\[ dp[i]=c[i]-c[i-1] \]
然后动动脑子(Author:没有nz),如果我们从dp[1]~dp[n]反向还原加起来就得到了原数组。

更有意义的是如果我们仅仅对dp数组进行修改,仅仅需要修改两个数——

假设修改区间\([x,y]\),就改dp[x]dp[y+1]

为啥呢?如果我们dp[x]+=num,相当于对后面所有的元素都产生了+num的影响,然后再通过dp[y+1]改回来就完了。

这跟树状数组有什么关系?

类比一下:因为我们树状数组修改父亲节点会对子节点产生一个影响...

对!修改前面的节点和后面的节点+1的地方!

但是注意我们修改不是向左(根)修改,是向右(叶子)修改。

如果想要单点修改和区间修改同时出现咋整?

单点p不就是假的区间[p,p]嘛!

我们开两种树状数组,一个是原序列的前缀和,另外一个就是前面说的差分数组。

然后操作一下差分的树状数组pos1=p,pos2=p+1,以及前缀和树状数组就OK了。

区间修改操作:
void modify(int pos,int num)
{//实现向右改数组的基本操作
    while (p<=n)//向右
    {
        sum[pos]+=num;
        pos+=lowbit(pos);
    }
}
void change_seg(int l,int r,int num)
{
    modify(l,num);//差分左侧
    modify(r,-num);//差分右侧
}

然后好像还有最后一个问题——

单点查询

由于我们差分了树状数组,单点查询就变成了区间查询的操作。

为啥?不是刚说了吗,前面所有的加起来不就是这个点的值嘛!

操作:
void ask_p(int pos)
{
    int res=0;
    while (pos>0)//限定左边
    {
        res+=a[pos];//还原现场
        pos-=lowbit(pos);//向左循环
    }
    return res;
}

所以我们得到了如下基本操作:

单点修改、单点查询、区间修改、区间查询。

总结一下,整个学习过程分为两个大的板块:

  1. 前缀和思想:单点修改+区间查询
  2. 差分数列思想:区间修改+单点查询。

然后我们甚至可以拿这个毒瘤数据结构解逆序对问题。

Tip:
离散化原先数组,然后食用本数据结构

你学会了吗?

End.

转载于:https://www.cnblogs.com/jelly123/p/10397835.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值