树状数组详解
树状数组
概念:
树状数据结构,通常维护一些带有差分性质的区间问题,比如单点加,单点乘,查询区间和,前后缀最大值、最小值等。
回顾线段树:
线段树得以快速修改和查询在于下传懒标记,而树状数组则是每次修改时,将标记上传,查询的时候不用像线段树一样每次 pushdown
。区别之处在于标记传送的方式的不同。
树状数组的图示:
和线段树不同,我们定义一个点,其儿子的范围(就是存的哪一个区间的范围)是 [ i − 2 k + 1 , i ] [i-2^k+1,i] [i−2k+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 x→x−1 转换成二进制,就会变成 k k k 之前的还是那样, k k k 这个位置为 0 , k k k 之后的位置都是 0。
比如 ( 10100 ) 2 − 1 10 → ( 10011 ) 2 (10100)_{2}-1_{10}→(10011)_2 (10100)2−110→(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 x→x+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=1rai−∑i=1l−1ai 的值。所以现在转换成如何在树状数组中求前缀的值。
首先,设区间 [ 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] [x−2a1+1,x] 的和,并且使 x → x − 2 a 1 x→x-2^{a_1} x→x−2a1 屡次操作就能求解答案。
时间复杂度分析:
由于每次操作都会使 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,q≤106 以及以下数据范围的题目。
一些例题:
树状数组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=1qr−l+1 次 add
操作,这样显然复杂度不对,但是这个题单点查询,而区间加,可以使用差分数组,回顾差分数组,令 d i = a i − a i − 1 d_i=a_i-a_{i-1} di=ai−ai−1 那么区间 [ 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,i≤mid 计算 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 l−1 个数中小于等于 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 l≤j≤r。然后还是和刚刚一样,需要主要把 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;
}