聊聊前缀和
比如数组
int a[7]={1,2,3,4,5,6,7}
如果需询问数组从第l个数到第r个数的和暴力做法时间复杂度为 O ( n ) O(n) O(n)
不过我们可以预处理一个前缀和数组
int b[7]={1,3,6,10,15,21,28}
比如要询问[l,r]区间的和我们可以这样做b[r]-b[l-1]这也时间复杂度为 O ( 1 ) O(1) O(1)
但是问题来了,如果我们要既要修改数组中元素的值,有要进行上述区间查询操作呢?
我们发现每次我们修改原数组中元素的值时间复杂度为 O ( 1 ) O(1) O(1)但是如果修改前缀和数组中元素的值时间复杂度将会退化到 O ( n ) O(n) O(n)
总结一下:
数组 | 修改元素的值时间复杂度 | 区间求和时间复杂度 |
---|---|---|
原数组 | O(1) | O(n) |
前缀和数组 | O(n) | O(1) |
我们可以发现如果需要单点更新和区间查询两种操作时间复杂度都是 O ( n ) O(n) O(n)
什么是树状数组?
树状数组是一种便于进行单点更新和区间查询的数据结构
树状数组相关操作
- 二进制中最后一个1—— l o w b i t lowbit lowbit
int lowbit(int x)
{
return x&-x;
}
-
单点更新
我们对数组位置为x的元素加上c
//树状数组为tree,数组元素个数为n,数组下标从0开始
void add(int x,int c)
{
for(;x<=n;x+=lowbit(x)) tree[x]+=c;
}
- 区间求和
//求出[1,x]数组中的总和即前缀和
int sum(int x)
{
int res=0;
for(;x;x-=lowbit(x)) res+=tree[x];
return res;
}
局限性
我们很容易发现上述树状数组只适用于单点更新和区间查询,但是如果是区间修改和单点查询好像力不从心
差分在树状数组中的应用
告诉你个好消息如果有差分的介入,那么树状数组可以进行区间更新和单点查询当然也可以进行更厉害的区间更新和区间查询。
区间更新、单点查询
我们把 t r e e [ ] tree[] tree[]数组构造成一个差分数组还是看题吧
题目
给定长度为N的数列A,然后输入M行操作指令。
第一类指令形如“C l r d”,表示把数列中第l~r个数都加d。
第二类指令形如“Q X”,表示询问数列中第x个数的值。
对于每个询问,输出一个整数表示答案。
输入格式
第一行包含两个整数N和M。
第二行包含N个整数A[i]。
接下来M行表示M条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=100010;
ll tree[N];
int n,m;
int lowbit(int x)
{
return x&-x;
}
void add(int x,int c)
{
for(;x<=n;x+=lowbit(x)) tree[x]+=c;
}
ll sum(int x)
{
int res=0;
for(;x;x-=lowbit(x)) res+=tree[x];
return res;
}
int main()
{
cin>>n>>m;
for(int i=1;i<=n;i++)
{
int a;
cin>>a;
add(i,a);
add(i+1,-a);
}
while(m--)
{
char t;
cin>>t;
if(t=='Q')
{
int x;
cin>>x;
cout<<sum(x)<<endl;
}
else
{
int a,b,c;
cin>>a>>b>>c;
add(a,c);
add(b+1,-c);
}
}
return 0;
}
我们可以发现对于上述代码即在建树的过程中建成差分树的形式即可
区间更新、区间查询
原数组a[],对于区间更新我们可以维护一个差分数组b[]
如果我们维护数组a的前缀和我们可以发现有下面等式:
∑
i
=
1
x
a
i
=
∑
i
=
1
x
∑
j
=
1
i
b
i
=
∑
i
=
1
x
(
x
−
i
+
1
)
b
i
\sum_{i=1}^x a_i=\sum_{i=1}^x\sum_{j=1}^i b_i=\sum_{i=1}^x(x-i+1)b_i
i=1∑xai=i=1∑xj=1∑ibi=i=1∑x(x−i+1)bi
变换一下:
∑
i
=
1
x
a
i
=
(
x
+
1
)
∑
i
=
1
x
b
i
−
∑
i
=
1
x
b
i
×
i
\sum_{i=1}^x a_i=(x+1)\sum_{i=1}^x b_i-\sum_{i=1}^x b_i×i
i=1∑xai=(x+1)i=1∑xbi−i=1∑xbi×i
于是我们可以维护两个差分树状数组 t r e e 1 [ ] tree1[] tree1[]维护$ bi 、 、 、 tree2[] 维 护 维护 维护i*bi$
给定一个长度为N的数列A,以及M条指令,每条指令可能是以下两种之一:
1、“C l r d”,表示把 A[l],A[l+1],…,A[r] 都加上 d。
2、“Q l r”,表示询问 数列中第 l~r 个数的和。
对于每个询问,输出一个整数表示答案。
输入格式
第一行两个整数N,M。
第二行N个整数A[i]。
接下来M行表示M条指令,每条指令的格式如题目描述所示。
输出格式
对于每个询问,输出一个整数表示答案。
每个答案占一行。
#include<iostream>
#include<algorithm>
#include<cstdio>
#include<cstring>
using namespace std;
typedef long long ll;
const int N=100010;
int n,m;
ll tree1[N],tree2[N]; //维护b[i] 维护i*b[i]
int lowbit(int x)
{
return x&-x;
}
void add(ll tree[],int x,ll c)
{
for(;x<=n;x+=lowbit(x)) tree[x]+=c;
}
ll sum(ll tree[],int x)
{
ll res=0;
for(;x;x-=lowbit(x)) res+=tree[x];
return res;
}
ll prefix_sum(int x)
{
return (x+1)*sum(tree1,x)-sum(tree2,x);
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)
{
int a;
scanf("%d",&a);
add(tree1,i,a);
add(tree1,i+1,-a);
add(tree2,i,1ll*i*a);
add(tree2,i+1,-1ll*(i+1)*a);
}
while(m--)
{
char t;
int l,r;
cin>>t>>l>>r;
if(t=='Q')
{
scanf("%d%d",&l,&r);
cout<<prefix_sum(r)-prefix_sum(l-1)<<endl;
}
else
{
int d;
scanf("%d",&d);
add(tree1,l,d),add(tree1,r+1,-d);
add(tree2,l,l*d),add(tree2, r+1,-1ll*(r+1)*d);
}
}
return 0;
}
树状数组应用
-
区间更新和区间查询
-
逆序对:首先有一种求逆序对的方法:开一个数组
cnt[n]
然后遍历数组中的每一个数,在cnt[]
数组中留下标记:比如数组中第i个元素为x,那就cnt[x]=1
,所谓逆序即:我比你先出现而且比你大,对于上述例子所谓比你大即在[x+1,n]这个区间中的值,所谓比你先出现即在第i-1次遍历时被标记过。转化一下相当于求 c n t cnt cnt数组中[x+1,n]区间内的和 -
第k小的数