树状数组
初步感受
已知 \(a_i\),求 \(\sum_{i=1}^7 a_i\)。
暴力:
\(ans=a_1+a_2+a_3+a_4+a_5+a_6+a_7\)
时间复杂度:\(O(n)\)
树状数组:
已知 \(A=\sum_{i=1}^4 a_i\),\(B=\sum_{i=5}^6 a_i\),\(C=\sum_{i=7}^7 a_i\),则 \(ans=A+B+C\)
时间复杂度:\(O(\log n)\)
这就是树状数组能快速求解信息的原因:我们总能将一段前缀 \([1,n]\) 拆成 不多于 \(\log n\) 段区间,使得这 \(\log n\) 段区间的信息是 已知的。
于是,我们只需合并这 \(\log n\) 段区间的信息,就可以得到答案。相比于原来直接合并 \(n\) 个信息,效率有了很大的提高。
树状数组具体长这样:
\(c\) 数组就是用来储存原始数组 \(a\) 某段区间的和的,也就是说,这些区间的信息是已知的,我们的目标就是把查询前缀拆成这些小区间。
我们先来感受一下树状数组是如何查询的。
\(e.g.\) 求 \(\sum_{i=1}^7 a_i\)
过程:从 \(c_7\) 开始跳,\(c_7\) 管辖的是 \([7,7]\),跳到 \(c_6\),\(c_6\) 管辖的是 \([5,6]\),跳到 \(c_4\),\(c_4\) 管辖的是 \([1,4]\),这就是 \([1,7]\)拆分而成的三个区间。\(ans=c_7+c_6+c_4\)。
\(e.g.\) 求 \(\sum_{i=5}^7 a_i\)
过程:可以发现询问的左端点并不是从 \(1\) 开始的,那我们可以用容斥原理拆询问。
因此原问题可以化为:\(\sum_{i=5}^7 a_i=\sum_{i=1}^7 a_i-\sum_{i=1}^4 a_i\),剩下的与上文类似,不再赘述。
管辖区间
我们规定,\(c_x\) 管辖的区间长度为 \(2^k\)。
其中 \(k\) 为 \(x\) 的二进制表示中最低为的 \(1\) 所在的二进制位数。
\(2^k\) 则是 \(x\) 的二进制表示中,最低位的 \(1\) 与它后面的 \(0\) 所构成的数。
\(e.g.\) \(c_6\) 管辖的是哪个区间?
\(6_{(2)}=110\),最低位的 \(1\) 与它后面的 \(0\) 所构成的数是 \(10\) ,即 \(2\),因此 \(c_6\) 管辖的区间长度是 \(6\),管辖的区间就是 \([5,6]\)。
lowbit函数
记 \(lowbit(x)\) 是 \(x\) 最低位的 \(1\) 与它后面的 \(0\) 所构成的数,\(lowbit(6)=2\)
因此 \(c_x\) 管辖的区间是 \([x-lowbit(x)+1,x]\)。
那么如何计算 \(lowbit\) 函数呢?显然 \(lowbit(x)=x \And -x\)
原理:
首先, \(-x\) 的二进制表示是 \(x\) 的二进制表示取反再加 \(1\)。
记 \(x_{(2)}=(...)10...0\),取反后是 \([...]01...1\),在加 \(1\) 就是 \(-x_{(2)}=[...]10...0\)。
由于 \((...)\) 与 \([...]\) 全部取反,因此 \((...) \And [...]=0\),所以 \(x \And -x=10...0=lowbit(x)\)。
int lowbit(int x){return x&-x;}
具体操作
接下来讲一下树状数组的一下操作。
单点修改
我们需要快速维护 \(c\) 数组。
显然,管辖 \(a_x\) 的 \(c_y\) 一定包含 \(c_x\),可以看看上面给的图。因此 \(y\) 是 \(x\) 的祖先,我们只需要每次从 \(x\) 开始往它的父亲跳,直到超出数组长度为止。
单点修改的具体过程:
1.修改 \(c_x\)。
2.令 \(x=x+lowbit(x)\),若 \(x>n\),终止循环。否则跳到第一步。
维护的区间信息与单点修改的种类,共同决定了 \(c_x\) 的修改方式。
-
若 \(c_x\) 维护的是区间和,修改种类是将 \(a_x\) 加上 \(k\),那么修改方式则是将 \(c_x\) 加上 \(k\)。
-
若 \(c_x\) 维护的是区间积,修改种类是将 \(a_x\) 乘上 \(k\),那么修改方式则是将 \(c_x\) 乘上 \(k\)。
然而,修改的种类与维护的区间信息并不一定是同种运算。
-
若 \(c_x\) 维护的是区间和,修改种类是将 \(a_x\) 赋为 \(k\),那么修改方式则是将 \(c_x\) 加上 \(k-a_x\)。
-
若 \(c_x\) 维护的是区间和,修改种类是将 \(a_x\) 乘上 \(k\),那么修改方式则是将 \(c_x\) 加上 \(a_x \times k-a_x\)
时间复杂度:\(O(\log n)\)
void change(int x,int k){
while(x<=N){
c[x]+=k;
x+=lowbit(x);
}
}
区间查询
回顾一下求 \(\sum_{i=5}^7 a_i\),这里我们用到了一个小技巧:
\(trick:\) $$\sum_{i=l}^r a_i=\sum_{i=1}^r a_i-\sum_{i=1}^{l=1} a_i$$
将区间 \([l,r]\) 的询问转换成 区间 \([1,r]\) 的信息减去区间 \([1,l-1]\) 的信息,那么只需要维护前缀询问。
至于前缀询问上文已提及,不再赘述。
查询区间 \([1,x]\) 的具体过程:
1.从 \(c_x\) 开始往前跳。
2.将 \(c_x\) 的贡献统计入答案中。
3.令 \(x=x-lowbit(x)\),若 \(x=0\) ,终止循环。否则跳到第一步。
时间复杂度:\(O(\log n)\)
int query(int x){
int ans=0;
while(x){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
建树
\(O(n \log n)\) 建树
根据给出的原序列,进行 \(n\) 次单点修改,即可完成建树。
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
change(i,a[i]);
}
\(O(n)\) 建树
- 每一个节点的值是由所有与自己直接相连的儿子的值求和得到的。因此可以倒着考虑贡献,即每次确定完儿子的值后,用自己的值更新自己的直接父亲。
void build(){
for(int x=1;<=n;x++){
c[x]+=a[x];
int fa=x+lowbit(x);
if(fa<=n)c[fa]+=c[x];
}
}
- \(c_x\) 的管辖范围是 \([x-lowbit(x)+1,x]\),因此先求出前缀和数组 \(sum\),在进行建树。
void build(){
for(int x=1;x<=n;x++){
c[x]=sum[x]-sum[x-lowbit(x)];
}
}
经典问题
单点修改区间查询
【题意】:给定一下两种操作:
\(1\) \(x\) \(k\) 把第 \(x\) 个数的值增加 \(k\)。
\(2\) \(x\) \(y\) 询问区间 \([x,y]\) 内所有数的和。
操作 \(1\) 显然是基础的单点修改。
操作 \(2\) 询问的是 \(\sum_{i=x}^y a_i=\sum_{i=1}^y a_i-\sum_{i=1}^{x-1} a_i\),可以用树状数组维护 \(\sum_{i=1}^n a_i\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N=1e6;
int n,m;
LL c[N+5];
int lowbit(int x){return x&-x;}
void change(int x,LL k){
while(x<=N){
c[x]+=k;
x+=lowbit(x);
}
}
LL query(int x){
LL ans=0;
while(x){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
int x;scanf("%d",&x);
change(i,x);
}
for(int i=1;i<=m;i++){
int op,x,y;LL c;scanf("%d",&op);
if(op==1){
scanf("%d%lld",&x,&c);
change(x,c);
}
else{
scanf("%d%d",&x,&y);
if(x>y)swap(x,y);
printf("%lld\n",query(y)-query(x-1));
}
}
return 0;
}
区间修改单点查询
【题意】:给定一下两种操作:
\(1\) \(x\) \(y\) \(k\) 把区间 \([x,y]\) 内所有数的值增加 \(k\)。
\(2\) \(x\) 询问第 \(x\) 个数的值。
对于操作 \(1\),暴力修改 \(O(n)\),显然是不行的。
考虑用树状数组维护差分数组,令 \(d_i=a_i-a_{i-1}\)。
我们分为以下三种情况来讨论。
-
\(l:\) \(a_l\) 的值加上了 \(k\),而 \(a_{l-1}\) 的值不变,\(d_l=(a_l+k)-a_{l-1}=(a_l-a_{l-1})+k\),因此 \(d_l\) 的值加上了 \(k\)。
-
\(r+1:\) \(a_{r+1}\) 的值不变,而 \(a_r\) 的值加上了 \(k\),\(d_{r+1}=a_{r+1}-(a_r+k)=(a_{r+1}-a{r})-k\),因此 \(d_{r+1}\) 的值减去了 \(k\),即加上 \(-k\)。
-
\((l,r]:\) \(a_i\) 的值加上了 \(k\),而 \(a_{i-1}\) 的值减去了 \(k\),\(d_i=(a_i+k)-(a_{i-1}+k)=a_i-a_{i-1}\),因此 \(d_i\) 的值不变。
分析后发现,我们只需要给 \(l\) 加上 \(k\),给 \(r+1\) 加上 \(-k\)。
对于操作 \(2\),\(a_i=\sum_{j=1}^i d_j\),只需要返回 \(d_j\) 的前缀和即可。
#include<bits/stdc++.h>
using namespace std;
const int N=100000;
int c[N+5],a[N+5];
int lowbit(int x){return x&-x;}
void change(int x,int k){
while(x<=N){
c[x]+=k;
x+=lowbit(x);
}
}
int query(int x){
int ans=0;
while(x){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
int main(){
int n,m;scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
change(i,a[i]-a[i-1]);
}
for(int i=1;i<=m;i++){
int op,x,y,c;scanf("%d",&op);
if(op==1){
scanf("%d%d%d",&x,&y,&c);
change(x,c),change(y+1,-c);
}
else{
scanf("%d",&x);
printf("%d\n",query(x));
}
}
return 0;
}
区间修改区间询问
【题意】:给定一下两种操作:
\(1\) \(x\) \(y\) \(k\) 把区间 \([x,y]\) 内所有数的值增加 \(k\)。
\(2\) \(x\) \(y\) 询问区间 \([x,y]\) 内所有数的和。
对于操作 \(2\),
\(\sum_{i=l}^r a_i\)显然是没办法直接用树状数组直接维护的。
考虑用容斥原理拆询问。
\(\sum_{i=l}^r a_i=\sum_{i=1}^r a_i-\sum_{i=1}^{l-1} a_i\)
可是如果你想直接用树状数组维护 \(\sum_{i=1}^r a_i\) 是没有办法做到区间修改的,我们继续往下看。
对于 \(\sum_{i=1}^r a_i\),考虑用差分数组表示 \(a_i\),
有:$$\sum_{i=1}^r \sum_{j=1}^i d_j$$
交换求和顺序,有:$$=\sum_{j=1}^r \sum_{i=j}^r d_j$$
令 \(i=j\),有:$$=\sum_{i=1}^r d_i \times (r-i+1)$$
令 \(solve(r)=(r+1) \times \sum_{i=1}^r d_i-\sum_{i=1}^r d_i \times i\),
因此:$$\sum_{i=1}^r \sum_{j=1}^i d_j=solve(r)$$
而题目问的是:$$\sum_{i=l}^r \sum_{j=1}^i d_j$$
对于:\(solve(r)=(r+1) \times \sum_{i=1}^r d_i-\sum_{i=1}^r d_i \times i\),
考虑用两个树状数组来维护。
第一个树状数组来维护 \(\sum_{i=1}^r d_i\),第二个树状数组来维护 \(\sum_{i=1}^r d_i \times i\)。
对于操作 \(1\),
对于第一个树状数组维护的差分数组 \(d_i=a_i-a_{i-1}\)。
我们分为以下三种情况来讨论。
-
\(l:\) \(a_l\) 的值加上了 \(k\),而 \(a_{l-1}\) 的值不变,\(d_l=(a_l+k)-a_{l-1}=(a_l-a_{l-1})+k\),因此 \(d_l\) 的值加上了 \(k\)。
-
\(r+1:\) \(a_{r+1}\) 的值不变,而 \(a_r\) 的值加上了 \(k\),\(d_{r+1}=a_{r+1}-(a_r+k)=(a_{r+1}-a{r})-k\),因此 \(d_{r+1}\) 的值减去了 \(k\),即加上 \(-k\)。
-
\((l,r]:\) \(a_i\) 的值加上了 \(k\),而 \(a_{i-1}\) 的值减去了 \(k\),\(d_i=(a_i+k)-(a_{i-1}+k)=a_i-a_{i-1}\),因此 \(d_i\) 的值不变。
分析后发现,我们只需要给 \(l\) 加上 \(k\),给 \(r+1\) 加上 \(-k\)。
而对于第二个树状数组,分析方法类似,只需要给 \(l\) 加上 \(k \times l\),给 \(r+1\) 加上 \(-k \times (r+1)\)。
#include<bits/stdc++.h>
using namespace std;
typedef long long LL;
const LL N=1e6;
LL c[N+5],cc[N+5],a[N+5];
LL lowbit(LL x){return x&-x;}
void change(LL x,LL k,LL kk){
while(x<=N){
c[x]+=k;
cc[x]+=kk;
x+=lowbit(x);
}
}
LL query(LL x){
LL ans=0;
while(x){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
LL getsum(LL x){
LL ans=0;
while(x){
ans+=cc[x];
x-=lowbit(x);
}
return ans;
}
LL solve(LL x){
return (x+1)*query(x)-getsum(x);
}
int main(){
LL n,m;scanf("%lld%lld",&n,&m);
for(LL i=1;i<=n;i++){
scanf("%lld",&a[i]);
change(i,a[i]-a[i-1],i*(a[i]-a[i-1]));
}
for(LL i=1;i<=m;i++){
LL op,x,y,c;
scanf("%lld",&op);
if(op==1){
scanf("%lld%lld%lld",&x,&y,&c);
change(x,c,x*c);
change(y+1,-c,(y+1)*(-c));
}
else{
scanf("%lld%lld",&x,&y);
printf("%lld\n",solve(y)-solve(x-1));
}
}
return 0;
}
逆序对
【题意】:若存在 \(i<j\) 并且 \(a_i>a_j\) ,则称之为逆序对。求逆序对数目。
考虑用树状数组维护一个桶,每个数出现就给桶的那个位置加 \(1\)。
如果让数从后往前遍历,则每次只需要查询有多少个比自己小的数,即查询桶的前缀和。
#include<bits/stdc++.h>
using namespace std;
const int N=5e5;
int a[N+5],c[N+5],n;
int ans=0;
int lowbit(int x){return x&-x;}
void change(int x,int k){
while(x<=n){
c[x]+=k;
x=x+lowbit(x);
}
}
int query(int x){
int res=0;
while(x){
res+=c[x];
x=x-lowbit(x);
}
return res;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++){
scanf("%d",&a[i]);
a[i]++;
}
for(int i=n;i>=1;i--){
change(a[i],1);
ans+=query(a[i]-1);
}
printf("%d",ans);
return 0;
}

浙公网安备 33010602011771号