树状数组小总结
**树状数组的操作以及运用**
小萌新学完树状数组神奇操作以后,初步认识到了数据结构的强大 tqi! 博客记录一下学习,树状数组主要是对数据的离散化处理,把原本n的复杂度降为logn,整体思想跟二分类似,用二进制的形式对数进行类似的处理。
1. 树状数组的离散化处理 (定义)
用d[]数组作为树状数组的存储空间,a[]为要处理的数组,si为数列a[]的前i项和(即前缀和,其实差分数组也是类似的),,树状数组一般用于存储a[]中的前缀和或者差分或者其他性质,将a[]中数据通过二进制进行离散化加快处理速度。举个例子,首先用“7”举例,二进制表示为0111,其中0111 -> 0110 -> 0100 -> 0000;,二进制的每一位代表一块数据的和,所以s7=d[7]+d[6]+d[4];也就是s7=d[0111]+d[0110]+d[0100];也就是将需要求和的数拆分成一个个二进制中'1'的表示(如上中'7'的拆分)。
而每一个d[x]中,x中二进制的最后一个'1'代表的数就是存储范围 ,如d[7]=d[0111],最后一个'1'代表2^0 =1,也就是范围为7,所以d[7]=a[7],又比如d[8]=d[1000],最后一个'1'代表2^3 =8,所以代表1-8范围,即d[8]=a[1]+a[2]+...+a[8];以此类推。
其中求和代码中怎么实现呢?:也就是每次让需要求和的数i 减去他的最后一位的'1' ,减到0结束循环过程如下:以7为例:0111-0001=0110; 0110-0010=0100; 0100-0100=0; 再用一个re存储d[x],最后返回re即可。其中得到最后一位1的数有个函数叫大名鼎鼎的“lowbit”函数;
lowbit函数: return x&(-x);
int lowbit(int x)
{
return x&(-x);
}
其中&为逻辑符号”与”操作,-x就是x的负数,具体操作可以百度(比我讲得好QAQ)。
使用过程如下 lowbit(7)=0001;(0111 lowbit-> 0001)
这下我们就可以写出树状数组的求和函数啦
int lowbit(int x)
{
return x&(-x);
}
int sum(int x)
{
int re=0;
while(x>0) // 直接写x也行一样的
{
re+=d[x];
x-=lowbit(x);
}
return re;
}
调用sum(x)就等于sx 即 sum(x) == sx == a[1]+a[2]+ ... + a[x]; 时间复杂度却变成了logn,类似与二分的思想。
2.树状数组的应用(单点修改,区间求和)
树状数组可以对一串数列a[]进行区间和单独的修改和查询,把时间复杂度从o(n)降成o(logn)
首先介绍一下 单点修改,区间求和 的操作 (可见洛谷树状数组模板1)
前面在介绍树状数组求和的sum函数中,我们可以求和sx,由于我们sx表示区间1-x的和,我们可以根据前缀和公式 -- s[i,j] = s[j] - s[i-1](很容易证明全部摆出来就可以消掉大部分)
所以在求区间[i,j]和是只要求出s[j]和s[i-1],也就是 sum[i,j] = sum[j] - sum[i-1]。 由于我们要用前缀和,故 我们用树状数组存储的就是原数组a[]的值
代码如下:
ll sum1=sum(y); //ll 为long long ,习惯了#define ll long long
ll sum2=sum(x-1);
ll ans=sum1-sum2;
printf("%lld\n",ans);
然后就是单点修改,如果要修改a[i]的值,只要把 包含a[i]的d[]都修改相同的量 即可。在d[]中,更新包含a[i]的d[];
怎么找?与sum函数过程是互逆过程,顺着lowbitx(x)往上加,每次加上原来的lowbit(x),**最后都会往2m上靠**,如果加到2m,依次就会走到2^(m+1) -> 2^(m+2) -> ... ->2M(2M<=n)。
如 更新a[1],从1开始 ,更新d[1],1本身等于20,因此以后就是21 (更新d[2])-> 2^2 -> 2^3 -> 2^4 ... 2m(2m<=n).
用代码实现就是说,从i开始,每次加上它的最后一位 也就是lowbit(i),就可以与sum形成逆运算(sum是从i -> 0,依次减掉lowbit(i) )
void add(ll x,ll v) // x为修改的位置a[x],v为修改的值value
{
while(x<=n)
{
d[x]+=v;
x+=lowbit(x);
}
}
这样调用add为单点修改,sum用前缀和性质也可以实现区间求和。
3.树状数组的应用(单点求值,区间修改) 参考题目:树状数组模板二
和上一个应用一样,要实现单点求值,区间修改,需要用到的是数列的 差分数组 ,所以我们 树状数组就用来保存数列a[]的差分数组d[]
为什么用差分数组呢? 原因很简单,因为差分数组可以对区间快速修改,而且单点查询也很容易。 公式证明如下,差分数组定义 :d[i] = a[i] - a[i-1] (当i==1时,a[0]=0),即d[1] = a[1]。
对于 单点求值 :需要求a[i],由于 d[i]=a[i]-a[i-1], d[i-1]=a[i-1]-d[i-2] , ... , d[2]=a[2]-a[1], d[1]=a[1]+0(a[0]),将上式左右分别相加,令数组d[]的前n项和为f[n],即累加结果为 :a[i]+0=f[i],即a[i]=f[i],所以这时我们的sum函数求到的结果就是我们单点查询的结果,式子也是一摸一样的 sum(i) == a[i] 。
同样对于 区间修改 得益于差分数组的优势,对于区间[i,j]的修改大小v,区间内的差分值依旧不变 ,只有左端点 d[i] = a[i] - a[i-1],由于a[i]修改了v,故修改以后变成了 *d[i]= a[i]+v-a[i-1] = d[i]+v,即 d[i]+v; ,只需要将d[i]也修改大小v即可,对于右端点 a[j]+v,又d[j+1]=a[j+1]-(a[j]+v),故只需要把d[j+1]减去v即可,即 d[j+1]-v;
这时我们就会发现,区间修改就可以用 add函数 对树状数组进行修改实现区间修改,把上叙式子化成代码,其实跟 2 中的操作几乎一样,只是树状数组中存储的数不同,函数也有了另外的含义,但是代码是一样的:
ll z,x,y,l;
scanf("%lld",&z);
if(z==1)
{
scanf("%lld%lld%lld",&x,&y,&l);
add(x,l); // d[x]+v;
add(y+1,-l); // d[y+1]-v;
}
else
{
scanf("%lld",&x);
ll ans=sum(x); //a[x]=sum(x)
printf("%lld\n",ans);
}
4.树状数组的构建和使用
前面两种用法均未讲解树状数组的构建built 其实都讲了没贴代码
其实 归根结底,树状数组其实是一种数据结构,优化时间复杂度的特殊数据结构( 简称工具 ),基本操作就是sum函数和add函数,一个用与求树状数组的和,一个用来更新树状数组中的数据,其他的都是看你怎么去使用它,上面两种其实就只是最常用的例子而已,结合差分或者前缀和实现一组数列的高效修改和更新 ,你要区间求和,就用前缀和,所以树状数组中就存放a[]本身值,用sum代表前缀和即可,你要区间修改,就用差分,所以树状数组中就存放差分数组d[],用sum函数实现a[]的体现,用add函数实现区间的修改
代码实现的话依次是:(存放a[]本身)
for(int i=1;i<=n;++i) add(i,a[i]);
存放差分数组d[], 把其当作一个区间长度为[i,i](长度为1)的修改本身的a[i]即可
for(int i=1;i<=n;++i)
{
add(i,a[i]);
add(i+1,-a[i]);
}
剩下的就是理解后自己对树状数组的灵活使用啦~ 放大招,对于数列a[]的区间修改,区间求和怎么去实现呢?
5.树状数组的区间修改和区间查询 区间修改,区间求和
学习完上面基本操作以后,就要调整树状数组的思考构建方式了 : 首先是要明确树状数组里面存放什么,需要做什么,做的事情可以转化为sum函数和add函数中去吗 ,根据这个就可以确定下树状数组中存储什么,也就是先考虑树状数组的构建。
对于这个题目,要进行区间修改,那肯定前缀和速度太慢,故舍弃,只能用差分数组进行修改。 但是问题来了,那差分数组如何实现快速区间求和呢?
前面可以知道,你如果用前缀和即可快速对区间进行查询,公式如下:
s[1,n]=s[1] - s[n] (si = a1 + a2 + ... + ai)
而这里我们只有差分数组 d[i] = a[i] - a[i-1]; f[i] = a[i]
发现关键联系了,差分的前n项和就是a[]的大小,也就是说区间求和可写成:
s[1,n] = a[1] + a[2] + ... + a[n] , f[i] = d[1] + d[2] + ... + d[i];
后面代入前面可得 :
s[1,n] = f[1] + ... + f[n]
= (d[1]) + (d[1] + d[2]) + ... + (d[1] + ... + d[n]);
为了与sum函数联系,我们配凑出一个f[i]:
s[1,n] = (n+1) * (d[1] + d[2] + ... + d[n]) - ( 1 * d[1] + 2 * d[2] + ... + n * d[n])
= (n+1) * sum(n) - F[n]
其中F[n]为数列{i * di}的前n项和
这个时候我们就需要第二个树状数组来保存数列{i * di},并用sum函数取它的前缀和;
那么代码来说就有 亿点点 的改动
ll d1[200000],d2[200000];
//d1存储a[i]差分数组d[i] ,d2储存d[i]*i.
ll n,m;
ll lowbit(ll x)
{
return x&(-x);
}
void add(ll a[],ll x,ll v)
//加个数组选择,其实可以写在一起,习惯就行
{
while(x<=n)
{
a[x]+=v;
x+=lowbit(x);
}
}
ll sum(ll a[],ll x)
{
ll re=0;
while(x)
{
re+=a[x];
x-=lowbit(x);
}
return re;
}
发现sum函数和add函数有稍微的改变,增加了一个数组选择,方便调用,其他一模一样。
构造修改代码
cin>>n;
for(ri i=1;i<=n;++i) add(d1,i,a[i]),add(d1,i+1,-a[i]); // ri = register int
for(ri i=1;i<=n;++i) add(d2,i,i*(a[i]-a[i-1]));
while(m--)
{
ll b;
cin>>b;
if(b==1)
{
ll x,y,k;
cin>>x>>y>>k;
add(d1,x,k),add(d1,y+1,-k);
add(d2,x,x*k),add(d2,y+1,-(y+1)*k);
}
else
{
ll x,y;
cin>>x>>y;
ll sum1=x*sum(d1,x-1)-sum(d2,x-1);
ll sum2=(y+1)*sum(d1,y)-sum(d2,y);
ll ans=sum2-sum1;
cout<<ans<<"\n";
}
}
这样一来就可以解决这个问题啦,也是对树状数组的灵活应用
最后一点,树状数组是工具,是数据结构,用法较为简单,要能自己灵活使用它,不能太拘泥于某种特殊情况