树状数组复习
文章目录
树状数组复习
功能
- 快速求前缀和 O ( l o g n ) O(logn) O(logn)
- 快速修改某数 O ( l o g n ) O(logn) O(logn)
原理
二进制拆分
x = 2 i k + 2 i k − 1 + ⋯ + 2 i 1 x=2^{i_k}+2^{i_{k-1}}+\dots+2^{i_1} x=2ik+2ik−1+⋯+2i1
i k ≥ i k − 1 ≥ ⋯ ≥ i 1 i_k \geq i_{k-1} \geq \dots \geq i_1 ik≥ik−1≥⋯≥i1
k ≤ l o g 2 x k \leq log_2x k≤log2x
-
( x − 2 i 1 , x ] (x-2^{i_1},x] (x−2i1,x]
-
( x − 2 i 1 − 2 i 2 , x − 2 i 1 ] (x-2^{i_1} -2^{i_2} , x-2^{i_1}] (x−2i1−2i2,x−2i1]
.
.
.
-
( 0 , x − 2 i 1 − 2 i 2 − ⋯ − 2 i k − 1 ] (0,x-2^{i_1}-2^{i_2}-\dots-2^{i_k-1}] (0,x−2i1−2i2−⋯−2ik−1]
显然
2
i
2^i
2i就是1<<lowbit(i)
为了求 ( l , r ] (l,r] (l,r]的和
我们用C[R]
表示sum[R-lowbit(R)+1][R]
所以我们最多有 n n n个区间
每次查询 ( l , r ] (l,r] (l,r]仅需查最多 l o g n logn logn个数
似乎不是很直观
放个图:
操作
初始化
for(int i=1;i<=n;i++) add(i,a[i])
复杂度 O ( l o g n ) O(logn) O(logn)
比较暴力…不过一般可以接受的
还有一种 O ( n ) O(n) O(n)的,不过比较麻烦
去求x的每个儿子是啥,利用儿子加边(只加树边)
for(int i=x-1;i;i-=lowbit(i)) c[x]+=c[i];
O ( n ) O(n) O(n)
还有一种比较简单的 O ( n ) O(n) O(n)的做法
从定义出发,c[x]=sum[x-lowbit(x)][x]
所以预处理一个前缀和
然后直接c[x]=s[x]-s[x-lowbit(x)]
就OK了
也非常简单
修改
对于修改操作
我们需要找到被修改的那个数影响到了哪些区间
其实就是找到该数的说有父节点
找父节点的操作:
p=x+lowbit(x)
最多改 l o g n logn logn次
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
查询
对于查询操作
每次累加lowbit(x)
位
最多加 l o g n logn logn次
for(int i=x;i;i-=lowbit(i)) sum+=tr[i];
应用
楼兰图腾
思路
把所有情况分成几类:
最低点是纵坐标 1 , 2 , 3 , … , n 1,2,3,\dots,n 1,2,3,…,n
用乘法原理就可以了
首先从左到右扫一遍,预处理处一个greater[k]
,即有多少点纵坐标比
y
k
y_k
yk大
同理,再求一个lower[k]
,有多少点比
y
k
y_k
yk大就OK了
树状数组tr[i]
保存的是坐标为i
的点的个数
高度是1到n,因此不用离散化
代码
/*************************************************************************
> File Name: p241楼兰图腾.cpp
> Author: typedef
> Mail: 1815979752@qq.com
> Created Time: 2020/11/21 21:00:02
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+3;
typedef long long ll;
int n;
int a[N];
int tr[N];
int great[N],lower[N];
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
return;
}
int sum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main(){
scanf("%d",&n);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++){
int y=a[i];
great[i]=sum(n)-sum(y);//有多少点纵坐标位于y~n之间
lower[i]=sum(y-1);//有多少点纵坐标比y小
add(y,1);
}
memset(tr,0,sizeof(tr));
ll res1=0,res2=0;//v,^;
for(int i=n;i>=1;i--){
int y=a[i];
res1+=great[i]*(ll)(sum(n)-sum(y));
res2+=lower[i]*(ll)(sum(y-1));
add(y,1);
}
printf("%lld %lld\n",res1,res2);
system("pause");
return 0;
}
[谜一样的牛](244. 谜一样的牛 - AcWing题库)
思路
假设 a 1 , a 2 , … , a n a_1,a_2,\dots,a_n a1,a2,…,an 为比第 i i i 头牛矮的牛的数量
容易得到 h n = a n + 1 h_n = a_n +1 hn=an+1
然后依次往前推
若当前推到了第 i i i 头牛
意味着该头牛的身高实在它前面 i − 1 i-1 i−1 t头牛中排行第 a i + 1 a_i+1 ai+1
这是我们只需找前面第 a i + 1 a_i+1 ai+1小 的数
我们需要完成如下操作
-
每次操作就是从剩余的数中找出第 k k k 小的数
-
删除某个数
显然要可以平衡树维护
但是我只会树状数组/kk
我们在建树的时候把每个身高设为一
用树状数组维护这个全一数组的前缀和
这个 1 1 1 表示该身高还可被用 1 1 1 次
删除操作就是在该位置减一
很容易实现n
那么如何完成第一个操作呢?
单用树状数组是无法完成的
不过,剩余的数是可以查询前缀和求出来的
不难发现:
操作一
⟺
\Longleftrightarrow
⟺ 找到一个最小的
x
x
x ,满足sum(x)==k
前缀和是单调递增的,因此可以二分查找
那么接下来就很简单了
复杂度 O ( m l o g 2 n ) O(mlog^2n) O(mlog2n) ,常数不大,甚至能跑赢平衡树
代码
/*************************************************************************
> File Name: p244谜一样的牛.cpp
> Author: typedef
> Mail: 1815979752@qq.com
> Created Time: 2020/11/24 22:09:46
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=100010;
int n;
int h[N];
int ans[N];
int tr[N];
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
return;
}
int sum(int x){
int res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main(){
scanf("%d",&n);
for(int i=2;i<=n;i++) scanf("%d",&h[i]);
for(int i=1;i<=n;i++) tr[i]=lowbit(i);
for(int i=n;i>=1;i--){
int k=h[i]+1;
int l=1,r=n;
while(l<r){
int mid=l+r>>1;
if(sum(mid)>=k) r=mid;
else l=mid+1;
}
ans[i]=r;
add(r,-1);
}
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
system("pause");
return 0;
}
进阶玩法ht
区间修改,单点查询
思路
为什么不用线段树?
其实很简单,利用差分思想
原数组a[N]
,树状数组(差分)b[N<<1]
操作
[
l
,
r
]
+
c
[l,r]+c
[l,r]+c
⟺
\Longleftrightarrow
⟺ b[l]+=c,b[r+1]-=c
查询a[x]
⟺
\Longleftrightarrow
⟺
b
[
1
]
+
b
[
2
]
+
⋯
+
b
[
x
]
b[1]+b[2]+\dots+b[x]
b[1]+b[2]+⋯+b[x]
代码
/*************************************************************************
> File Name: 树状数组pro.cpp
> Author: typedef
> Mail: 1815979752@qq.com
> Created Time: 2020/11/21 21:44:55
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+7;
typedef long long ll;
int n,m;//区间长度&操作个数
int a[N];
ll tr[N];
int lowbit(int x){
return x&-x;
}
void add(int x,int c){
for(int i=c;i<=n;i+=lowbit(i)) tr[i]+=c;
return;
}
ll sum(int x){
ll res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
int main(){
memset(tr,0,sizeof(tr));
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
for(int i=1;i<=n;i++) add(i,a[i]-a[i-1]);
while(m--){
char op[2];
int l,r,d;
scanf("%s%d",op,&l);
if(*op=='C'){
scanf("%d%d",&r,&d);
add(l,d),add(r+1,-d);
}
else{
printf("%lld\n",sum(l));
}
}
system("pause");
return 0;
}
区间修改,区间查询
思路
线段树它不香吗?
修改:
操作
[
l
.
r
]
+
c
[l.r]+c
[l.r]+c
⟺
\Longleftrightarrow
⟺ b[l]+=c,b[r+1]-=c
还算简单吧…
查询:
首先考虑 a 1 + a 2 + ⋯ + a x a_1+a_2+\dots+a_x a1+a2+⋯+ax
原式 = = = ∑ i = 1 x \sum^{x}_{i=1} ∑i=1x = = = ∑ i = 1 x ∑ j = 1 i b i \sum^{x}_{i=1} \sum^{i}_{j=1} bi ∑i=1x∑j=1ibi
for(int i=1;i<=x;i++)
for(int j=1;j<=i;j++)
+bj
还比较好理解吧…
①:
num | value |
---|---|
1 | b 1 b_1 b1 |
2 | b 1 + b 2 b_1+b_2 b1+b2 |
3 | b 1 + b 2 + b 3 b_1+b_2+b_3 b1+b2+b3 |
x | b 1 + b 2 + ⋯ + b x b_1+b_2+\dots+b_x b1+b2+⋯+bx |
这玩意像个三角形
直接求不好求
运用补集思想
②:
num | value |
---|---|
0 | b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2+⋯+bn |
1 | b 2 + b 3 + ⋯ + b n b_2+b_3+\dots+b_n b2+b3+⋯+bn |
2 | b 3 + b 4 + ⋯ + b n b_3+b_4+\dots+b_n b3+b4+⋯+bn |
3 | b 4 + b 5 + ⋯ + b n b_4+b_5+\dots+b_n b4+b5+⋯+bn |
x | 0 0 0 |
③:
num | value |
---|---|
0 | b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2+⋯+bn |
1 | b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2+⋯+bn |
2 | b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2+⋯+bn |
3 | b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2+⋯+bn |
x | b 1 + b 2 + ⋯ + b n b_1+b_2+\dots+b_n b1+b2+⋯+bn |
我们让 ③-② 可得 ①
因此①= ( b 1 + ⋯ + b x ) × ( x + 1 ) − ( b 1 + 2 b 2 + 3 b 3 + ⋯ + x b x ) (b_1+\dots+b_x)\times(x+1)-(b_1+2b_2+3b_3+\dots+xb_x) (b1+⋯+bx)×(x+1)−(b1+2b2+3b3+⋯+xbx)
前面用 b i b_i bi 的前缀和求,后面用 i × b i i \times b_i i×bi 的前缀和
因此
我们不得不用
tr1[]
表示
b
i
b_i
bi 的前缀合
tr2[]
表示
i
×
b
i
i \times b_i
i×bi 的前缀合
然后就很简单了
真TM的恶心
代码
这玩意儿写起来真上头
小心爆int
/*************************************************************************
> File Name: 树状数组promax.cpp
> Author: typedef
> Mail: 1815979752@qq.com
> Created Time: 2020/11/22 21:18:03
************************************************************************/
#include<bits/stdc++.h>
using namespace std;
const int N=100007;
typedef long long ll;
ll tr1[N],tr2[N];//b数组前缀和,bi乘i的前缀和;
int a[N];
int n,m;
int lowbit(int x){
return x&-x;
}
void add(ll tr[],int x,ll c){
for(int i=x;i<=n;i+=lowbit(i)) tr[i]+=c;
return;
}
ll sum(ll tr[],int x){
ll res=0;
for(int i=x;i;i-=lowbit(i)) res+=tr[i];
return res;
}
ll prefix_sum(int x){
return sum(tr1,x)*(x+1)-sum(tr2,x);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++) scanf("%d",a+i);
for(int i=1;i<=n;i++){
int b=a[i]-a[i-1];
add(tr1,i,b);
add(tr2,i,(ll)i*b);
}
while(m--){
char op[2];
int l,r,d;
scanf("%s%d%d",op,&l,&r);
if(*op=='Q'){
printf("%lld\n",prefix_sum(r)-prefix_sum(l-1));
}
else{
scanf("%d",&d);
add(tr1,l,d);
add(tr1,r+1,-d);
add(tr2,l,l*d);
add(tr2,r+1,(r+1)*-d);
}
}
return 0;
}