浅谈树状数组
本文章同步发表在博客园。
树状数组是一种好吃的东西,建议大家学了以后多吃吃喵。(什
算法详解
定义
树状数组是一种支持单点修改和可差分区间查询,并且码量低常数还小的非常赞的数据结构。
原理
首先我们都知道,任何一个数都可以表示成至多
这种思想就是树状数组的核心。
树状数组中,利用
::::info[x&-x 快速计算数字
树状数组中具体是如何处理的呢?它定义了
定义懂了,那么要单点修改或者查询前缀和分别该怎么做呢?
首先说单点修改。在修改第
再来说查询前缀和。假设你要查询的是
实现
void upd(int x,int k){
while(x<=n)
c[x]+=k,x+=x&-x;
return;
}//单点修改
int ask(int x){
int sum=0;
while(x)sum+=c[x],x-=x&-x;
return sum;
}//前缀和查询
int query(int l,int r){
return ask(r)-ask(l-1);
}//区间查询
运用
求逆序对数量
使用权值树状数组,维护每个数出现的次数,就像桶一样。顺序遍历 upd(a[i],1); 把
以下代码中假设 其实核心代码就两行。
for(int i=1;i<=n;i++)
Ans+=ask(n)-ask(a[i]),upd(a[i],1);
求区间最值
求区间最值其实一般不会用树状数组来着,可以考虑用线段树,不带修可以用 ST 表。
但是硬要用树状数组还是可以用的,只是时间复杂度有两只
单点修改时,枚举到每个
以下代码中以求
void upd(int x,LL k) {
a[x]=k;
while(x<=n){
c[x]=a[x];
for(int i=1;i<(x&-x);i<<=1)
c[x]=max(c[x],c[x-i]),
x+=(x&-x);
}return;
}//单点修改
LL ask(int l,int r){
LL res=a[r];
while(l<=r){
if(r-(r&-r)+1<l)res=max(res,a[r--]);
else res=max(res,c[r]),r-=(r&-r);
}return res;
}//区间查询
区间修改单点查询
你以为这个不能用树状数组维护?只能用线段树?当然不是啦!
你可以用树状数组维护原数列的差分数组,区间修改时调用 upd(l,k);upd(r+1,-k);,查询的时候 ask(x) 就可以直接得到原数列中
区间修改区间查询
哇这个真的很神。那以后估计是没人用线段树搞这个了,它被抛弃咯(
依然是维护原数列的差分数组,求原序列的区间和等价于求原序列的前缀和。原序列是差分数组的前缀和,所以我们需要求的是差分数组的前缀和的前缀和。
假设我们要求原序列的前
记差分数组第
拆括号变形:
注意到第一项可以直接通过
权值树状数组上二分求第 k 小
与倍增的思想很类似,也很像求 LCA 的过程。从
LL Kth(LL k){
LL res=0,cnt=0;
for(int i=20;i>=0;i--){
if((res+(1<<i))>Mx)continue;
if(cnt+c[res+(1<<i)]<k)
res+=(1<<i),cnt+=c[res];
}
return res+1;
}
例题讲解
P1168 中位数
其实这个东西也可以用堆做。
考虑维护权值树状数组,但是需要先离散化。
如果当前被加入树状数组的数字恰好是奇数个,也就是我们要求解答案的位置了,就使用二分,二分得出最小且出现个数
时间复杂度好像有两只
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 1e5+5;
LL n,a[N],cnt,c[N],bel[N];
map<LL,LL> Ls;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void upd(int x,LL k){while(x<=cnt)c[x]+=k,x+=x&-x;return;}
LL ask(LL x){LL sum=0;while(x)sum+=c[x],x-=x&-x;return sum;}
int main(){
n=read();
for(int i=1;i<=n;i++)a[i]=read(),Ls[a[i]]=0;
for(auto &u:Ls)u.se=(++cnt),bel[cnt]=u.fr;
for(int i=1;i<=n;i++)a[i]=Ls[a[i]];
for(int i=1;i<=n;i++){
upd(a[i],1);
if(i%2==0)continue;
int l=1,r=cnt,res=0;
while(l<=r){
int mid=(l+r)/2;
LL sum=ask(mid);
if(sum>i/2)res=mid,r=mid-1;
else l=mid+1;
}cout<<bel[res]<<"\n";
}
return 0;
}
P2345 MooFest G
首先根据坐标点排序。
根据一个点来算,听力左边比它小的和右边比它小的,分开跑两次树状数组。
树状数组不仅要维护听力比它差的奶牛个数,还要维护它们的坐标点之和。于是还得开两个树状数组。
第一次是正序遍历,枚举到一个
第二次是倒序遍历,枚举到一个
记得开 long long。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 2e4+5;
struct cow{LL id,val;}a[N];
LL n,cnt,c[N],cc[N],Ans,Mx;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(cow c1,cow c2){return c1.id<c2.id;}
void upd(int x,LL k){while(x<=Mx)c[x]+=k,x+=x&-x;return;}
LL ask(LL x){LL sum=0;while(x)sum+=c[x],x-=x&-x;return sum;}
void Upd(int x,LL k){while(x<=Mx)cc[x]+=k,x+=x&-x;return;}
LL Ask(LL x){LL sum=0;while(x)sum+=cc[x],x-=x&-x;return sum;}
int main(){
n=read();
for(int i=1;i<=n;i++)
a[i].val=read(),a[i].id=read();
sort(a+1,a+n+1,cmp);
for(int i=1;i<=n;i++)
Mx=max(Mx,a[i].val);
for(int i=1;i<=n;i++){
LL cnt=Ask(a[i].val);
Ans+=(cnt*a[i].id-ask(a[i].val))*a[i].val;
upd(a[i].val,a[i].id);
Upd(a[i].val,1);
}for(int i=1;i<=Mx;i++)c[i]=0,cc[i]=0;
for(int i=n;i>=1;i--){
LL cnt=Ask(a[i].val-1);
Ans+=(ask(a[i].val-1)-cnt*a[i].id)*a[i].val;
upd(a[i].val,a[i].id);
Upd(a[i].val,1);
}cout<<Ans<<"\n";
return 0;
}
P3531 LIT-Letters
交换相邻两个数是什么概念啊,你说得对,逆序对!但是是求什么东西的逆序对呢?
找出
为什么?因为我们要通过交换使得
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 1e6+5;
LL n,a[N],Ans,c[27][N],cnt[27],d[27],p[N];
string s,t;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void upd(int x,int k){while(x<=n)p[x]+=k,x+=x&-x;return;}
int ask(int x){int sum=0;while(x)sum+=p[x],x-=x&-x;return sum;}
int main(){
n=read();
cin>>s>>t;s=" "+s,t=" "+t;
for(int i=1;i<=n;i++){
int x=s[i]-'A'+1;
c[x][++cnt[x]]=i;
}for(int i=1;i<=n;i++){
int x=t[i]-'A'+1;
a[i]=c[x][++d[x]];
}for(int i=1;i<=n;i++){
Ans+=ask(n)-ask(a[i]-1);
upd(a[i],1);
}cout<<Ans<<"\n";
return 0;
}
P4392 Sound 静音问题
发现这题可以直接套用上面提到的【求区间最值】的方式做。
但是不太对啊,这题的区间是一个滑动窗口的形式,是不是可以更简单地解决呢?
当然可以。同步维护权值树状数组,滑动窗口,某个值被移进来了就让其对应数值个数
这个时候你就要问了我们怎么求区间最大和最小呢?很简单,注意到可以用上面提到的【权值树状数组上二分求第
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 1e6+5;
LL n,m,can,Mx,a[N],c[N],Anscnt;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
void upd(int x,LL k){while(x<=Mx)c[x]+=k,x+=x&-x;return;}
LL ask(LL x){LL sum=0;while(x)sum+=c[x],x-=x&-x;return sum;}
LL Kth(LL k){
LL res=0,cnt=0;
for(int i=20;i>=0;i--){
if((res+(1<<i))>Mx)continue;
if(cnt+c[res+(1<<i)]<k)
res+=(1<<i),cnt+=c[res];
}
return res+1;
}
int main(){
n=read(),m=read(),can=read();
for(int i=1;i<=n;i++)
a[i]=read()+1,Mx=max(Mx,a[i]);
for(int i=1;i<=n;i++){
upd(a[i],1);
if(i-m>=1)upd(a[i-m],-1);
if(i<m)continue;
LL D=Kth(m)-Kth(1);
if(D<=can){cout<<i-m+1<<"\n";Anscnt++;}
}if(!Anscnt)cout<<"NONE\n";
return 0;
}
CF703D Mishka and Interesting sum
好玩的题。
不难发现,如果题目要求的是区间内出现奇数次的数字的异或和,那么这个题目直接用前缀异或和就可以解决了,因为偶数次会抵消。
但是现在要求的却是偶数次的,所以我们需要再异或上该区间内不重复的数字异或和。
那现在的问题就转化为,求一个区间内去重后所有数字的异或和。
发现这个东西非常莫队,但是数据范围有
首先得把所有数字放进树状数组,维护当前每个位置的值,便于快速查找区间异或和。
离线,把所有询问按照右端点升序排序,然后依次处理每个操作。
遍历
问题来了,怎么保证出现新的之后把旧的抹去不会影响后面统计答案?因为你已经按照右端点升序排序,所以不用担心后面出现包含旧的却不包含新的的情况。如果包含旧的,一定包含新的,因为右端点是升序排序的啦!
最后还原查询顺序输出即可,整体还是非常简单的。
注意需要不断维护每种数字最后一次出现的位置。
#include<bits/stdc++.h>
#define LL long long
#define UInt unsigned int
#define ULL unsigned long long
#define LD long double
#define pii pair<int,int>
#define pLL pair<LL,LL>
#define pDD pair<LD,LD>
#define fr first
#define se second
#define pb push_back
#define isr insert
using namespace std;
const int N = 1e6+5;
struct query{LL l,r,ans,id;}q[N];
LL n,m,a[N],s[N],c[N],now;
map<LL,LL> lst;
LL read(){
LL su=0,pp=1;char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')pp=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){su=su*10+ch-'0';ch=getchar();}
return su*pp;
}
bool cmp(query q1,query q2){return q1.r<q2.r;}
bool cmp2(query q1,query q2){return q1.id<q2.id;}
void upd(int x){LL p=a[x];while(x<=n)c[x]^=p,x+=x&-x;return;}
LL ask(int x){LL sum=0;while(x)sum^=c[x],x-=x&-x;return sum;}
int main(){
n=read();
for(int i=1;i<=n;i++)
a[i]=read(),s[i]=s[i-1]^a[i],upd(i);
m=read();
for(int i=1;i<=m;i++)
q[i].l=read(),q[i].r=read(),q[i].id=i;
sort(q+1,q+m+1,cmp);
for(int i=1;i<=m;i++){
while(now<q[i].r){
now++;
if(lst[a[now]]!=now){
if(lst[a[now]])
upd(lst[a[now]]),a[lst[a[now]]]=0;
lst[a[now]]=now;
}
}q[i].ans=ask(q[i].r)^ask(q[i].l-1);
q[i].ans^=s[q[i].r]^s[q[i].l-1];
}sort(q+1,q+m+1,cmp2);
for(int i=1;i<=m;i++)cout<<q[i].ans<<"\n";
return 0;
}
简单总结
树状数组,是一种好用的
码这么多字也不容易,还麻烦你留个赞支持一下,真是太感谢啦!