一、为什么需要树状数组:
当面对:1.修改或者查询某个元素的数值,2.获取某个区域里面的数组和等问题时,用暴力的方法会超时的时候,我们便可以尝试使用树状数组来解决问题。
二、树状数组是什么?怎么来的?
在回答这个问题之前,我们先想一个问题:求数组的前n项和。对于这个问题我们大家第一想法就是暴力求解一个一个的加起来呗。这样确实可以但时间复杂度上可能会高一点,那有没有其它的方法呢?能不能用空间换时间呢?答案是有的。如图:
图1
将其变成:
图2
这样就类似于树一样,我们只需要求根节点的值就可以获得前n项的和了:
图3
但我们真的需要这样构建一个树么?其实仔细观察一下图2,如果我们要求前三个元素或者前四个元素和的时候23这一区域内的值是没有用的,因为我们可以通过上一层的36来减去13推断出来它的值。这样推断看来,其实每一层的第偶个元素其实都是没有存在的必要的了因此我们可以把它们去除掉然后构成新的数组:
图四
此时的数组的大小就跟原来的数组一样的大小了,而且每次修改特定的元素值时,只需要自底向上的修改包括他的区间即可,例如:如果修改7为8,就需要修改27的值为28,75的值为76。那该如何用代码来实现树状数组的创建呢?
先观察一下树状数组的一些特征:
图5
可以发现当把树状数组从1开始拍成一个序列后,每个数组所在位置的变成二进制后的第一个1(包括1)以后的值在各个层之间是不同的,但在层里面的值是相同的。(x表示的第几个元素)
lowbit(x)函数就是图中表红二进制的值,换句话说就是len值。原理大概类似于是:
补码变为原码的过程中就是在从右到左到第一个1(包括)不变,从第一个1的左侧全变一下即可得到原码,这样俩个码在第一个1前面的各个位上的与的结果就为0,而在第一个1的后面因为为0就全为0,这样便可以得到这个值了。(不考虑符号位了)
举个例子:8 的补码:0000 1000
-8的补码:1111 1000
俩个一与的补码:0000 1000
这样便能得到8了。
代码的实现:
int lowbit(int i)
{
return i & -i;
}
可以发现:1.每个节点设为T[x]的父节点为T[x+lowbit(x)]。例如:T[2]的父结为T[2+lowbit(2)]=T[4]
T[4]的父节点为T[4+lowbit(4)]=T[8] 。
2.T[x]的值:先观察第一层的树状数组(lowbit(i)的值为1):就是原数组i=i-lowbit(i)+1的值(本身)
第二层的树状数组(lowbit(i)的值为2包含俩个元素):原数组从i-lowbit(i)+1到i(i-lowbit(i)+2)之间的和
第三层的树状树组(lowbit(i)的值为4包含四个元素):原数组从i-lowbit(i)+1到i(i-lowbit(i)+4)之间的和故: T[x]=
代码的实现:
int a[100];
int T[100];
int n;
int lowbit(int i)
{
return i & -i;
}
void tmake()//树状数组的构成
{
int i;
int j;
for (i = 1; i <= n; i++)
{
for (j = i - lowbit(i) + 1; j <= i; j++)
{
T[i] += a[j];
}
}
}
三、单点修改(当原数组内的值被改变后该如何更新树状数组内的值呢?)
可以利用:1.每个节点设为T[x]的父节点为T[x+lowbit(x)]。例如:T[2]的父结为T[2+lowbit(2)]=T[4]
T[4]的父节点为T[4+lowbit(4)]=T[8] 。然后把每个包含他的父节点都给更新一下即可。
int a[100];
int T[100];
int n;
int lowbit(int i)
{
return i & -i;
}
void add(int k, int x)//k表示第k个,x表示增加了多少
{
int i=k;//从第k个开始逐渐向上遍历
while (i <= n)
{
T[i] += x;
i +=lowbit(i);
}
}
四、区间求和(给出一个区间[x,y]该如何求出这个区域里面的和呢(原数组中的))?
为了解决这个问题我们可以先解决前y项和,然后再求前x-1项之和把他俩一减即可。
图解:
例如假设数组下标从1开始,要求区间[3,5]之间的和,可以先求前5项的和为sum2=15,然后再求前2项的和为:sum1=3,所以区间[3,5]之间的和为:sum=sum2-sum1=12.
解决思路:既然方法已经明确了该如何用代码实现呢?
可以观察一下树状数组,可以发现以下规律:
前一项和:就是T[1].(1-lowbit(1)=0)
前俩项和:T[2],(2-lowbit(2)=0)
前三项和:T[2]+T[1],(3-lowbit(3)=2,2-lowbit(2)=0)
前四项和:T[4],(4-lowbit(4)=0)
前五项和:T[4]+T[5](5-lowbit(5)=4,4-lowbit(4)=0)
可以发现:前n项的lowbit(n)代表这个树状数组T[n]表示的这个从原数组中下表(n-lowbit(n),n]之内的原数组和为多少,然后直至n-lowbit(n)为0即可一结束了.
代码实现:
int ask(int x)
{
int i=x;
int sum=0;
while (i-lowbit(i)!=0)
{
sum += T[i];
i -= lowbit(i);
}
sum += T[i];//加这个是为了当有i-lowbit(i)==0,会加漏掉,例如2-lowbit(2)==0,此时sum就没加上所以要补这一条
return sum;
}
到时候求解区间[x,y]之间的和只需要:ask(y)-ask(x-1),即可不过得先判断x-1是否大于等于1,如果小于1,直接ask(y)即可。
五、区间增加(如果对区间[x,y]之间都加上k呢?那又该如何操作呢?)
我们可以引入一个差分数组d[i],d[i]+=a[i]-a[i-1](a[0]=0),因为d[1]=a[1]-a[0],
d[2]=a[2]-a[1],d[3]=a[3]-a[2]……而a[3]=d[1]+d[2]+d[3],所以a[x]=
而先在a[1,y]之间的和用d[]数组进行表示则为:
sumy=a1+a2+……+ay=+
+……+
=
,
sumx=a1+a2+……+ax-1=
然后再按照规律可以发现:sum=sumy-sumx。
此时就将它构造成了矩形,然后蓝色区域的值为:矩形的面积-白色区域的面积,此时我们需要用树状数组来表示d[i]和i*d[i]这俩个数组,
并用b[i]数组来表示i*d[i值的数组。
然后再分析一下:如果a[x,y]之间加上了k,则在d[i]数组中:d[x]会加上k,d[y+1]会减上k,
同理在b[i]数组中:b[x]=x*d[x]因为d[x]加上了k,所以b[x]会多kx,b[y+1]会少k*(y+1)。
此时若用td[i]树状数组来表示d[i]数组,tb[i]树状数组来表示b[i]数组。知道了每个元素的增加量以后可以直接模仿上述代码中的add()调用俩次即可。
代码的实现:
void addtd(int k, int x)//k表示第k个,x表示增加了多少
{
int i=k;//从第k个开始逐渐向上遍历
while (i <= n)
{
td[i] += x;
i +=lowbit(i);
}
}
void addtb(int k, int x)
{
int i = k;//从第k个开始逐渐向上遍历
while (i <= n)
{
tb[i] += x*k;
i += lowbit(i);
}
}
void add_tb(int l,int r,int x)
{
addtb(l,l*x);
addtb(r+1,-x*(r+1));
}
void add_td(int l, int r, int x)
{
addtd(l, x);
addtd(r + 1, -x);
}
六、逆序对问题
问题:给定长度为 n 的序列a
,求 a
中满足 i<j
且 a[i]>a[j]
的数对 (i,j)
的数量。
题目链接:P1908 逆序对 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
例如:长度为6的序列a={5,4,2,6,3,1},首先我们可以暴力的解决答案,但暴力的方法可能会超时,那不妨我们思考一下其它的方法:构造个b[n]数组(b[n]数组表示的是a序列中的每个数在b[]数组出现的次数,例如a[1]=5,那么代表5出现了一次则,b[5]+=1),然后把a[n]数组从尾部遍历到开头(这样只需要求b[0,x-1]的和即可代表比b[a[x]]小的即可)
具体:a[6]=1,然后统计b[0]到b[a[6]-1]的和为sum1=0,然后b[a[6]]+=1,b{0,1,0,0,0,0,0}
a[5]=3,然后统计b[0]到b[a[5]-1]的和为sum2=1,然后b[a[5]]+=1,b{0,1,0,1,0,0,0}
a[4]=6,然后统计b[0]到b[a[4]-1]的和为sum3=2,然后b[a[4]]+=1,b{0,1,0,1,0,0,1}
a[3]=2,然后统计b[0]到b[a[3]-1]的和为sum4=1,然后b[a[3]]+=1,b{0,1,1,1,0,0,1}
a[2]=4,然后统计b[0]到b[a[2]-1]的和为sum5=3,然后b[a[2]]+=1,b{0,1,1,1,1,0,1}
a[1]=5,然后统计b[0]到b[a[1]-1]的和为sum6=4,然后b[a[1]]+=1,b{0,1,1,1,1,1,1}
sum==11
但这样开出来的b[]的数组大小会不确定了,因为它会随着a[]数组中的最大值进行开辟空间,所以我们这时候可以采用离散化的方法,来减少空间的开辟。那么什么是离散化呢?
我举个例子:如果a={50,1,99},此时我们可以简化表达将其按照在a数组里的大小关系进行划分
a[1]=50,在三个数中第二大,所以a[1]=2,a[2]=1,在三个数中第三大,所以a[2]=1,
a[3]=99,在三个数中第一大,所以a[3]=3,这样改为了减少b[]空间的开辟,同时也不会影响最终的结果。
而为了实现离散化的方法,我们可以用结构体来实现:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define N 500001
int b[N];
int T[N];
struct node
{
int data;
int num;
};
然后再对a[]进行按照(data)小到大进行排序的函数:
bool com(node c, node d)
{
if (c.data == d.data)//如果俩个值一样大,就按照顺序来判断
{
return c.num < d.num;
}
return c.data < d.data;
}
然后对结构体数组中:data用来存储数据,num代表顺序。
for (i = 1; i <= n; i++)
{
cin >> a[i].data;
a[i].num = i;
}
sort(a.begin() + 1, a.begin() + n+1, com);//进行从小到大的排序
for (i = 1; i <= n; i++)
{
c[a[i].num]=i;
}//用c数组来表示这里面的值
然后再对排完序后的a[]里面的内容进行输出到c容器中,举个例子:假设a={99,12,1}
num: 1,2,3
经过按照data排序后:a={1,12,99}
num: 3, 2, 1
a[1]中所存储的数据就是最小的,而它在容器a中的原位置为a[1].num=3,那么对应到c[]中的位置也就不能更改还应该为:a[1].num=3而且c[a[1].num]=1(因为它的数据最小),重复以上步骤直至到n,则c={3,2,1}就是对a中数据的离散化处理.
后续就是对b[]数组进行建树状数组,然后再根据倒序c[n]进行对树状数组的加1,然后求前缀和即可。
tmake(n);
i = n;
while (i>0)
{
if (i == 0)
{
break;
}
add(c[i], 1, n);
sum += ask(c[i]- 1);
i--;
}
全部代码:
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
#define N 500001
int b[N];
int T[N];
struct node
{
int data;
int num;
};
int lowbit(int x)
{
return x & -x;
}
bool com(node c, node d)
{
if (c.data == d.data)
{
return c.num < d.num;
}
return c.data < d.data;
}
void tmake(int n)
{
int i, j;
for (i = 1; i <= n; i++)
{
for (j = i - lowbit(i) + 1; j <= i; j++)
{
T[i] += b[j];
}
}
}
int ask(int x)
{
int i = x;
int sum = 0;
while (i - lowbit(i) != 0)
{
sum += T[i];
i -= lowbit(i);
}
sum += T[i];//加这个是为了当有i-lowbit(i)==0,会加漏掉,例如2-lowbit(2)==0,此时sum就没加上所以要补这一条
return sum;
}
void add(int k, int x,int n)//k表示第k个,x表示增加了多少
{
int i = k;//从第k个开始逐渐向上遍历
while (i <= n)
{
T[i] += x;
i += lowbit(i);
}
}
int main()
{
int n;
cin >> n;
int i;
vector<struct node>a(n + 1);
vector<int>c(n + 1);
long long sum = 0;
for (i = 1; i <= n; i++)
{
cin >> a[i].data;
a[i].num = i;
}
sort(a.begin() + 1, a.begin() + n+1, com);//进行从小到大的排序
for (i = 1; i <= n; i++)
{
c[a[i].num]=i;
}//用c数组来表示这里面的值
tmake(n);
i = n;
while (i>0)
{
if (i == 0)
{
break;
}
add(c[i], 1, n);
sum += ask(c[i]- 1);
i--;
}
cout << sum;
}
参考链接:1.五分钟丝滑动画讲解 | 树状数组_哔哩哔哩_bilibili
2.〔manim | 算法 | 数据结构〕 完全理解并深入应用树状数组 | 支持多种动态维护区间操作_哔哩哔哩_bilibili 3.树状数组 - OI Wiki (oi-wiki.org)