当前位置: 首页 > news >正文

树状数组详解

树状数组

概念:

树状数据结构,通常维护一些带有差分性质的区间问题,比如单点加,单点乘,查询区间和,前后缀最大值、最小值等。

回顾线段树:

线段树得以快速修改和查询在于下传懒标记,而树状数组则是每次修改时,将标记上传,查询的时候不用像线段树一样每次 pushdown。区别之处在于标记传送的方式的不同。

树状数组的图示:

和线段树不同,我们定义一个点,其儿子的范围(就是存的哪一个区间的范围)是 [ i − 2 k + 1 , i ] [i-2^k+1,i] [i2k+1,i] 其中 k k k i i i 二进制下从后往前第一个为 1 1 1 的位置,下标从0开始计算,我们称这个值为 l o w b i t i lowbit_i lowbiti

比如: l o w b i t 3 = l o w b i t ( 11 ) 2 = 2 0 = 1 lowbit_3=lowbit_{(11)_2}=2^0=1 lowbit3=lowbit(11)2=20=1

先解决如何快速计算 l o w b i t lowbit lowbit 首先暴力枚举一个二进制位数,然后判断这一位在原数的二进制下是否是1,这样是 log ⁡ n \log n logn 的,有点慢了。

考虑二进制的一些性质,因为二进制数也有着进位退位这个说法,假设一个数 x x x 它二进制下从后往前第一个 1 出现的位置为 k k k 那么如果将 x → x − 1 x→x-1 xx1 转换成二进制,就会变成 k k k 之前的还是那样, k k k 这个位置为 0 , k k k 之后的位置都是 0。

比如 ( 10100 ) 2 − 1 10 → ( 10011 ) 2 (10100)_{2}-1_{10}→(10011)_2 (10100)2110(10011)2

这个时候我们再跟原数 x x x 进行异或操作,就变成了 10100 x o r 10010 10100 xor 10010 10100xor10010 就变成了 00111 00111 00111 然后再跟原数 x x x 取 & 操作,也就是把 k k k 之后那些没用的 0 给挤掉。

int lowbit(int x){return x&(x^(x-1));
}

这样有点复杂啊,怎么让他好看点呢?

由于补码是反码+1,也就是 10100 10100 10100

反码: 01011 01011 01011

补码: 01100 01100 01100

好玩的事情出现了,我们发现补码的+1会一直进位,什么时候不进位呢,显然就是进位到一个位置 k k k 这个地方为0,就会停止进位,而这个0正是原数反码中的从后往前的第一个出现的1的位置,这样把两个数 & 起来结果就对了。

那么知道了这些,树状数组有哪些性质呢?

  • 对于节点 i i i 其直接父亲编号为 i + l o w b i t i i+lowbit_i i+lowbiti

那么修改的时候我们就可以把所有的 x > i x>i x>i 满足 x x x 中有 i i i 的信息都给打上标记。

单点修改打标记的时间复杂度:

由于我们发现 x → x + l o w b i t x x→x+lowbit_x xx+lowbitx 的操作实际上是进位操作,所以最多会进行 log ⁡ 2 V \log_2 V log2V 次操作, V V V 是树状数组节点个数,也就是树状数组值域。

这样修改操作就可以了,我们看查询操作:

前文已经提及了,树状数组需要满足差分性质,比如求区间和就有差分性质,求区间 ∑ i = l r a i \sum_{i=l}^r a_i i=lrai 的值,就可以求 ∑ i = 1 r a i − ∑ i = 1 l − 1 a i \sum_{i=1}^ra_i-\sum_{i=1}^{l-1}a_i i=1raii=1l1ai 的值。所以现在转换成如何在树状数组中求前缀的值。

首先,设区间 [ 1 , x ] [1,x] [1,x] x x x 的二进制分解是 2 a 1 + 2 a 2 + 2 a 3 + . . . + 2 a k , a 1 < a 2 < . . . a k 2^{a_1}+2^{a_2}+2^{a3}+...+2^{a_k},a_1<a_2<...a_k 2a1+2a2+2a3+...+2ak,a1<a2<...ak 那么转化成对于一个数 x x x 求解 [ x − 2 a 1 + 1 , x ] [x-2^{a_1}+1,x] [x2a1+1,x] 的和,并且使 x → x − 2 a 1 x→x-2^{a_1} xx2a1 屡次操作就能求解答案。

时间复杂度分析:

由于每次操作都会使 x x x 的二进制中 1 的个数少一个,所以总的时间复杂度为 log ⁡ 2 x \log_2 x log2x 的。

假设 V V V n n n 同阶,则总的时间复杂度为 O ( n + q ) log ⁡ 2 n O(n+q)\log_2 n O(n+q)log2n 的,常数极小,通常可以在1s内解决 n , q ≤ 1 0 6 n,q\leq 10^6 n,q106 以及以下数据范围的题目。

一些例题:

树状数组1

和上文讲述一样,这个例题不再复述。

#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define pre(i,l,r) for(int i=(l);i>=(r);--i)
const int N=5e5+5;
int sum[N],a[N],n,m,x,k,op;//sum是树状数组的数组,a是原来的数组 
void add(int x,int v){//单点修改,将x的位置的值加上vwhile(x<N){sum[x]+=v;x+=x&-x;} 
}
void ask(int x,int &ans){//询问前缀和[1,x] while(x){ans+=sum[x];x-=x&-x;}
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);cin>>n>>m;rep(i,1,n) cin>>a[i],add(i,a[i]);rep(i,1,m){cin>>op>>x>>k;if(op==1) add(x,k);else{int l=0,r=0;ask(k,r);ask(x-1,l);cout<<r-l<<'\n';}}return 0;
}
//tomxi

树状数组2

这道题大一眼看好像不能做,因为有区间修改,如果按照第一种方法我们需要进行 ∑ i = 1 q r − l + 1 \sum_{i=1}^q r-l+1 i=1qrl+1add 操作,这样显然复杂度不对,但是这个题单点查询,而区间加,可以使用差分数组,回顾差分数组,令 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=aiai1 那么区间 [ l , r ] [l,r] [l,r] 统一加上 k k k 实际上就是差分数组 l l l 的位置加 k k k 然后 r + 1 r+1 r+1 的位置减去 k k k,查询的时候直接查询差分数组前缀和。

代码:

#include<bits/stdc++.h>
using namespace std;
#define int long long
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define pre(i,l,r) for(int i=(l);i>=(r);--i)
const int N=5e5+5;
int sum[N],a[N],n,m,x,y,k,op;//sum是树状数组的数组,a是原来的数组 
void add(int x,int v){//单点修改,将x的位置的值加上vwhile(x<N){sum[x]+=v;x+=x&-x;} 
}
void ask(int x,int &ans){//询问前缀和[1,x] while(x){ans+=sum[x];x-=x&-x;}
}
int32_t main(){ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);cin>>n>>m;rep(i,1,n) cin>>a[i],add(i,a[i]-a[i-1]);rep(i,1,m){cin>>op>>x;if(op==1){cin>>y>>k;add(x,k);add(y+1,-k);}else{int ans=0;ask(x,ans);cout<<ans<<'\n';}}return 0;
}
//tomxi

逆序对

之前讲过归并排序的逆序对,就是分治的时候计算右边对左边产生的影响,准确来说对于分治区间 [ l , r ] [l,r] [l,r] 我们对于每一个 i , i ≤ m i d i,i\leq mid i,imid 计算 j , j > m i d j,j>mid j,j>mid 满足 a i > a j a_i>a_j ai>aj 的个数。这样是 O ( n log ⁡ n ) O(n\log n) O(nlogn) 的。我们也可以在 O ( n log ⁡ 2 n ) O(n\log^2 n) O(nlog2n) 的时间复杂度内计算逆序对,使用树状数组(当然也可以 O ( n log ⁡ n ) O(n\log n) O(nlogn) 用树状数组,等会儿会讲。)。

直接做显然不行,因为 V V V 很大,我们空间是开不下的,我们将 A A A 数组离散化记离散化后 B i = r a n k ( A i ) B_i=rank(A_i) Bi=rank(Ai),然后进行刚刚我们说的那样做。

由于树状数组常数极小,所以 n log ⁡ 2 n n\log^2 n nlog2n 也过了。

#include<bits/stdc++.h>
using namespace std;
#define int long long 
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define pre(i,l,r) for(int i=(l);i>=(r);--i)
const int N=5e5+5;
int b[N],a[N],n,s[N],ans=0,sum=0;
/*
b是离散化数组,a是原来的数组,s是树状数组的数组 
*/
void add(int x,int v){while(x<N){s[x]+=v;x+=x&-x;}
} 
void ask(int x,int &res){while(x){res+=s[x];x-=x&-x;}
}
void solve(int l,int r){if(l>=r) return;/*只有一个数显然不能构成逆序对 */int mid=(l+r)>>1;solve(l,mid);solve(mid+1,r);rep(i,mid+1,r) add(a[i],1);rep(i,l,mid){sum=0;ask(a[i]-1,sum);/*求[mid+1,r]有多少数满足<a[i] */ans+=sum;}rep(i,mid+1,r) add(a[i],-1);/*递归完这层要还原 */
}
int32_t main(){ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);cin>>n;rep(i,1,n) cin>>a[i],b[i]=a[i];sort(b+1,b+n+1);int k=unique(b+1,b+n+1)-b-1;rep(i,1,n) a[i]=lower_bound(b+1,b+k+1,a[i])-b;solve(1,n);cout<<ans; return 0;
}
//tomxi

然后我们考虑怎么变成单 log ⁡ \log log 其实比两个 log ⁡ \log log 的要简单。

我们从后往前扫,这样能保证当前树状数组内的值都满足其下标是在 i i i 后面的,这样保证了第一维,然后树状数组查询所有小于 A i A_i Ai 的加和就是答案。

这个东西我们可以叫他一维偏序。

#include<bits/stdc++.h>
using namespace std;
#define int long long 
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define pre(i,l,r) for(int i=(l);i>=(r);--i)
const int N=5e5+5;
int b[N],a[N],n,s[N],ans=0,sum=0;
/*
b是离散化数组,a是原来的数组,s是树状数组的数组 
*/
void add(int x,int v){while(x<N){s[x]+=v;x+=x&-x;}
} 
void ask(int x,int &res){while(x){res+=s[x];x-=x&-x;}
}int32_t main(){ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);cin>>n;rep(i,1,n) cin>>a[i],b[i]=a[i];sort(b+1,b+n+1);int k=unique(b+1,b+n+1)-b-1;rep(i,1,n) a[i]=lower_bound(b+1,b+k+1,a[i])-b;pre(i,n,1){sum=0;ask(a[i]-1,sum);ans+=sum;add(a[i],1);}cout<<ans<<'\n'; return 0;
}
//tomxi

然后树桩数组可以解决一些二维数点之类的问题,与可持久化线段树不同的是,树状数组需要离线下来做,也是用其差分性。

比如这个题

我们把他分成前 r r r 个数中小于等于 x x x 的数减去前 l − 1 l-1 l1 个数中小于等于 x x x 的数的个数,然后这个可以使用树状数组求前缀和。

#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define pre(i,l,r) for(int i=(l);i>=(r);--i)
const int N=2e6+5;
int n,m,a[N],s[N],cnt=0,ans[N];
struct ask{int k,id,op;
};
void add(int x,int k){while(x<N){s[x]+=k;x+=x&-x;}
}
void query(int x,int &ans){while(x){ans+=s[x];x-=x&-x;}
}
vector<ask> upd[N];
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);cin>>n>>m;rep(i,1,n) cin>>a[i];rep(i,1,m){int l,r,k;cin>>l>>r>>k;upd[l-1].push_back({k,i,-1});upd[r].push_back({k,i,1});}rep(i,1,n){add(a[i],1);for(auto it:upd[i]){int res=0;query(it.k,res);ans[it.id]+=res*it.op;}}rep(i,1,m) cout<<ans[i]<<'\n';return 0;
}
//tomxi

来道离线二维数点练练手:

HH的项链

经典的离线下来,设 p r e i pre_i prei 表示 a i a_i ai 上一次出现的下标,然后区间查询实际上就是查找 p r e j < l pre_j<l prej<l j j j 的个数 l ≤ j ≤ r l\leq j\leq r ljr。然后还是和刚刚一样,需要主要把 p r e i pre_i prei 统一加上一,因为如果 p r e i = 0 pre_i=0 prei=0 修改的时候 l o w b i t lowbit lowbit 就一直是 0 死循环了。

代码:

#include<bits/stdc++.h>
using namespace std;
#define rep(i,l,r) for(int i=(l);i<=(r);++i)
#define pre(i,l,r) for(int i=(l);i>=(r);--i)
const int N=1e6+5;
int n,m,a[N],s[N],cnt=0,ans[N],pre[N],lst[N];
struct ask{int k,id,op;
};
void add(int x,int k){while(x<N){s[x]+=k;x+=x&-x;}
}
void query(int x,int &ans){while(x){ans+=s[x];x-=x&-x;}
}
vector<ask> upd[N];
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);cin>>n;rep(i,1,n) cin>>a[i];rep(i,1,n){pre[i]=lst[a[i]]+1;lst[a[i]]=i;}cin>>m;rep(i,1,m){int l,r;cin>>l>>r;upd[l-1].push_back({l,i,-1});upd[r].push_back({l,i,1});}rep(i,1,n){add(pre[i],1);for(auto it:upd[i]){int res=0;query(it.k,res);ans[it.id]+=res*it.op;}}rep(i,1,m) cout<<ans[i]<<'\n';return 0;
}
//tomxi

比莫队快了将近五倍。

三维偏序:

#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5;
struct node{int a,b,c;
}t[N];
struct Node{int b,c,cnt,ans;
}p[N];
int n,k,s[N],ans[N];
void upd(int x,int k){while(x<N){s[x]+=k;x+=x&-x;}
}
int query(int x){int ans=0;while(x){ans+=s[x];x-=x&-x;}return ans;
}
void solve(int l,int r){if(l>=r) return;int mid=(l+r)>>1;solve(l,mid);solve(mid+1,r);sort(p+l,p+mid+1,[](const Node&x,const Node&y){return ((x.b==y.b)?x.c<y.c:x.b<y.b);});sort(p+mid+1,p+r+1,[](const Node&x,const Node&y){return ((x.b==y.b)?x.c<y.c:x.b<y.b);});int i=l,j=mid+1;/*i是左边区间的指针,j是右边区间的指针 */while(j<=r){while(i<=mid&&p[i].b<=p[j].b){upd(p[i].c,p[i].cnt);++i;}p[j].ans+=query(p[j].c);++j;}for(int k=l;k<i;++k) upd(p[k].c,-p[k].cnt);
}
int main(){ios::sync_with_stdio(false);cin.tie(nullptr);cout.tie(nullptr);cin>>n>>k;for(int i=1;i<=n;++i) cin>>t[i].a>>t[i].b>>t[i].c;int cnt=0;sort(t+1,t+n+1,[](const node&l,const node&r){return ((l.a==r.a)?(l.b==r.b)?l.c<r.c:l.b<r.b:l.a<r.a);});for(int i=1;i<=n;++i){if(t[i].a==t[i-1].a&&t[i].b==t[i-1].b&&t[i].c==t[i-1].c){p[cnt].cnt++;}else{p[++cnt]={t[i].b,t[i].c,1,0};}}solve(1,cnt);for(int i=1;i<=cnt;++i){ans[p[i].ans+p[i].cnt-1]+=p[i].cnt;}for(int i=0;i<n;++i) cout<<ans[i]<<'\n';return 0;
}

相关文章:

  • 在Linux虚拟机下使用vscode,#include无法跳转问题
  • ZYNQ笔记(十四):基于 BRAM 的 PS、PL 数据交互
  • 【Token系列】01 | Token不是词:GPT如何切分语言的最小单元
  • 云服务器 —— 公有 IP 与 私有 IP
  • 【计算机视觉】CV项目实战- 深度解析TorchVision_Maskrcnn:基于PyTorch的实例分割实战指南
  • 深入解析Spring Boot配置处理器:机制、架构与实践
  • 【计算机网络】信息时代的数字神经系统
  • NVLink、UALink 崛起,PCIe Gen6 如何用 PAM4 迎战未来?
  • 基于QT的仿QQ音乐播放器
  • 极简桌面app官网版下载 极简桌面最新版 安装包下载
  • 栈相关算法题解题思路与代码实现分享
  • 深入解析NuttX:为何它是嵌入式RTOS领域的标杆?​​
  • 思科路由器重分发(RIP动态路由+静态路由)
  • RAG技术与应用---0426
  • 8.学习笔记-Maven进阶(P82-P89)
  • 23种设计模式-行为型模式之观察者模式(Java版本)
  • 零基础上手Python数据分析 (24):Scikit-learn 机器学习初步 - 让数据预测未来!
  • stm32L4R5ZI Nucleo-144 GPIO点灯及按键中断
  • Log4j Properties 配置项详细说明
  • linux socket编程之tcp(实现客户端和服务端消息的发送和接收)
  • 哈马斯官员:只要以军持续占领,哈马斯就不会放下武器
  • 坤莹·帕塔玛·利斯达特拉任世界羽联主席
  • 强政神鸟——故宫里的乌鸦
  • 国家数据发展研究院在京正式揭牌
  • 特朗普称已为俄乌问题设最后期限,届时美国态度或生变
  • 魏晓栋已任上海崇明区委常委、组织部部长