树状数组总结
可以支持 单点修改, 查询的数据结构。一个挺有用的数据结构。原理请见 Oi-Wiki。
基本的代码:
int c[N];
int lowbit(int x) {
// x 的二进制中,最低位的 1 以及后面所有 0 组成的数。
return x & -x;
}
int sum(int x) { // a[1]..a[x]的和
int p = 0;
for(;x;x-=lowbit(x)) p+=c[x];
return ans;
}
void upd(int x, int k) {
for(;x<=n;x+=lowbit(x) c[x]+=k;
}
区间修改,单点查询
其实就是差分的前缀和。把差分的好的数据丢进去,再进行修改,得到的和就是原数组经过修改的值。也可以这样,将原数列设为 。将修改后的数组加上 就好了。
#include<bits/stdc++.h>
using namespace std;
int n,m,o,x,y,k,a[500005],c[500005];
int lowbit(x) {
return x&-x;
}
void update(int x,int y) {
for(;x<=n;x+=lowbit(x)) c[x]+=y;
}
int check(int x) {
int p=0;
for(;x;x-=lowbit(x)) p+=c[x];
return p;
}
int main() {
cin>>n>>m;
for(int i=1;i<=n;i++) scanf("%d",&a[i]);
while(m--) {
scanf("%d",&o);
if(o==1) {
scanf("%d%d%d",&x,&y,&k);
update(x,k);
update(y+1,-k);
}
else {
scanf("%d",&x);
printf("%d\n",a[x]+check(x));
}
}
return 0;
}
区间修改,区间查询
对于区间修改,非常熟悉,直接定义差分数组 。便有 。设 ,带入上式得:。观察该式,可得每个 加了 次,即:
现在就可以使用两个树状数组 和 ,分别维护 的前缀和( 是固定的,查询时再乘上)。
如何应用
现在要在 中,全部加 。在 中就是在 。放到 中分别是:
- .
- .
代码
// 适用 P3372 【模板】线段树 1
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N = 1e6;
int c[2][N],a[N],n,m,op,x,y,k;
int lowbit(int x) {
return x& -x;
}
void add(int p,int v) {
for(int i=p;i<=n;i+=lowbit(i)) {
c[0][i]+=v,c[1][i]+=v*p;//这里不是 i,i会改变
}
}
int query(int p) {
int res=0;
for(int i=p;i;i-=lowbit(i)) {
res+=c[0][i]*(p+1)-c[1][i];
}
return res;
}
signed main() {
cin>>n>>m;
for(int i=1;i<=n;i++) {
cin>>a[i];
add(i,a[i]-a[i-1]);
}
for(int i=1;i<=m;i++) {
cin>>op>>x>>y;
if(op==1) {
cin>>k;
add(x,k);
add(y+1,-k);
}
else {
cout<<query(y)-query(x-1)<<"\n";
}
}
return 0;
}
二维树状数组
单点修改,矩阵查询
对于二维的树状数组,我们设 为左上角为 ,右下角为 的矩阵。先行后列,我们就可以理解为嵌套的树状数组。
对于单点修改,需要将所有包含 的 都修改一遍。
对于查询,使用类似二维前缀和的方法。
void upd(int x,int y,int k) {//将 a[x][y] 加 k
for(int i=x;i<=n;i+=lowbit(i)) {
for(int j=y;j<=n;j+=lowbit(j)) {
c[i][j]+=k;
}
}
}
int s(int xf,int yf,int xl,int yl) {//求 a[xf][yf]~a[xl][yl]的和
return ss(xl,yl)-ss(xf-1,yl)-ss(xl,yf-1)+ss(xf-1,yf-1);
}
int ss(int x,int y) {
int p=0;
for(int i=n;i>=x;i-=lowbit(i)) {
for(int j=n;j>=y;j-=lowbit(j)) {
p+=c[i][j];
}
}
return p;
}
矩阵修改,矩阵查询
基于一维数组的区间修改,区间查询。我们同样也可以在二维数组中差分,为:。
现在要查询左上角为 ,右下角为 的矩阵和可以表示为:
考虑化简这个式子。对于 ,当 时,他就会被累加一次。 有 个值, 有 个取值。乘法原理一用,得:(以下的 为原来的 )
将不会变的 和会变的 分开:
按照一样的思想,把定值放在最后处理。维护 。
实现:
typedef long long ll;
ll t1[N][N],t2[N][N],t3[N][N],t4[N][N];
void add(llx,lly,ll z) {
for(int X=x;X<=n;X+=lowbit(X)){
for(int Y=y;Y<=m;Y+=lowbit(Y)){
t1[X][Y]+=z;
t2[X][Y]+=z*x;//注意是z*x而不是z*X,后面同理
t3[X][Y]+=z*y;
t4[X][Y]+=z*x*y;
}
}
}
void range_add(ll xa,ll ya,ll xb,ll yb,ll z){//(xa,ya)到(xb,yb)子矩阵
add(xa,ya,z);
add(xa,yb+1,-z);
add(xb+1,ya,-z);
add(xb+1,yb+1,z);
}
ll ask(ll x,ll y){
ll res=0;
for(int i=x;i;i-=lowbit(i))
for(int j=y;j;j-=lowbit(j))
res+=(x+1)*(y+1)*t1[i][j]-(y+1)*t2[i][j]-(x+1)*t3[i][j]+t4[i][j];
return res;
}
ll range_ask(ll xa,ll ya,ll xb,ll yb){
return ask(xb,yb)-ask(xb,ya-1)-ask(xa-1,yb)+ask(xa-1,ya-1);
}
权值树状数组
对一个序列的权值数组 构建树状数组。
权值数组
序列每个数出现的次数,类似于数组计数。如 1 1 4 5 1 4
,权值数组就是 3 0 0 2 1
。
小技巧
当值域很大,并且关注数的大小关系时,可以离散化。
问题 1
修改操作:要将 从 修改为 ,其实就是 ,。
我们可以用这种方法解决单点修改,全局 小值问题(具体可见LOG,本文不探讨此题的实现)。
考虑如何查询 小值,如果用前缀和,每次二分查询 ,( 是前缀和)那么查询是 。但是修改要对权值数组重新前缀和,在线处理特别慢。但是树状数组可以实现前缀和的功能。这样的复杂度就是 。虽然实现了加速,但还有优化的空间。
此时就要考虑到树状数组美好的性质,查询 的树状数组是 的,但是某些值只需要查询一次就好了。比如我们要查询 ,只需要访问 就好了。
那现在有 。如果 ,那这个区间就是 。考虑如何满足这样的性质。将 分为若干个 ,很符合我们的倍增思想!
以下设 为当前区间内数的数量, 是当前区间末尾,从大到小枚举 :
- 如果满足 ,将 ,。
- 如果不满足,就尝试更小的 。
模拟1 1 1 1 1 1 1
,要找第 小数。
开始 。
找到 满足,。(, 管辖 )
找到 满足,。(, 管辖 )
会发现,由于倍增从大到小,在二进制中也就越后。所以新加的区间一定满足 。复杂度得到降低:。
Code
int find(int k) {
int x=0,sum=0;
for(int i=__lg(len);~i;i--)
//__lg 就是 log2,但是时间复杂度为Θ(1)
if(x+(1<<i)<len&&sum+c[x+(1<<i)]<k)
//这里不能越界
x+=1<<i,sum+=c[x];
//最后要再 +1 可参考倍增 LCA
return x+1;
}
了解了一些基础的理论知识,让我们来看道题吧。
题意如下: 你要维护数据结构,支持以下总次数为 的 6 种操作:
- 插入一个数
- 删除一个数
- 查询 的排名(排名为比 小的数个数 )
- 查询排名为 的数
- 查询比 小的最大的数
- 查询比 大的最小的数
对于 的数据,
呦呦呦,这不平衡树吗
其实树状数组可以用很短的代码(不到 1KB)解决本题。首先离散化下。
1,2 用权值树状数组秒杀。
第 3 个只要查询 的数的数量,其实就是求 的前缀和, 。
4 不就是求 小值么。
5,6 差不多。但实现上有一点区别。考虑转化成问题 3,4。
-
对于 5。先求比 小的数的排名(记作 ),就变成查询 小值。
-
对于 6。求比 大的数的排名(记作 )。也变成查询 小值。
时间复杂度 。并且常数小,码量小。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n,a[N],b[N],rk[N],c[N],cnt,len;
int lowbit(int x)
{
return x&-x;
}
int wh(int x)
{
return lower_bound(rk+1,rk+len+1,x)-rk;
}
void upd(int x,int z)
{
for(;x<=len;x+=lowbit(x)) c[x]+=z;
}
int s(int x)
{
int p=0;
for(;x;x-=lowbit(x))
p+=c[x];
return p;
}
int find(int k) {
int x=0,sum=0;
for(int i=__lg(len);~i;i--)
if(x+(1<<i)<len&&sum+c[x+(1<<i)]<k)
x+=1<<i,sum+=c[x];
return x+1;
}
int main() {
cin>>n;
for(int i=1;i<=n;i++)
{
cin>>a[i]>>b[i];
if(a[i]!=4) rk[++cnt]=b[i];
}
sort(rk+1,rk+cnt+1);
len=unique(rk+1,rk+cnt+1)-(rk+1);
//对权值离散化,以下提到的权值数组均为离散化后的权值
for(int i=1;i<=n;i++)
{
switch(a[i])
{
//wh 为权值在权值数组中的下标
case 1:upd(wh(b[i]),1);break;
case 2:upd(wh(b[i]),-1);break;
//离散化后的权值是连续的,所以 wh(b[i])-1 就是第一个 <x 的数
case 3:cout<<s(wh(b[i])-1)+1<<"\n";break;
//查询 k 小值
case 4:cout<<rk[find(b[i])]<<"\n";break;
//s(wh(b[i])-1) 为第一个 <x 的数的数量。然后查询 s(wh(b[i])-1) 小值就好了
case 5:cout<<rk[find(s(wh(b[i])-1))]<<"\n";break;
//s(wh(b[i])) 为 ≤x 的数的数量,+1 后为比 x 大的数的排名。查询这个排名的值就好了
case 6:cout<<rk[find(s(wh(b[i]))+1)]<<"\n";break;
/*
rk[i] 就是将权值数组变回实际权值
*/
}
}
return 0;
}
问题 2
一点应用
逆序对,就是指在一个序列中 的数。很多时候使用归并排序解决。其实树状数组也可以。步骤如下:
- 离散化一下。
- 用树状数组记录权值为 的个数。对于 ,逆序对为 的个数。即 (为离散化后个数,也是最大的权值)。
- 求完后再将 加入树状数组。
#include<bits/stdc++.h>
using namespace std;
#define int long long
int t[500005],a[500005],rk[500005],s[500005],n,ans;
int lowbit(int x) {
return x & -x;
}
void upd(int i,int k) {
for(;i<=n;i+=lowbit(i)) t[i]+=k;
}
int ss(int i) {
int p=0;
for(;i;i-=lowbit(i)) p+=t[i];
return p;
}
signed main() {
scanf("%lld",&n);
for(int i=1;i<=n;i++) {
scanf("%lld",&a[i]);
rk[i]=a[i];
}
sort(rk+1,rk+n+1);
int len=unique(rk+1,rk+n+1)-(rk+1);
for(int i=1;i<=n;i++)
s[i]=lower_bound(rk+1,rk+len+1,a[i])-rk;
for(int i=n;i>=1;i--) {
ans+=ss(s[i]-1);
upd(s[i],1);
}
cout<<ans;
return 0;
}
小技巧
建树
可以把每个元素都当成单点修改,复杂度 。
但……仅仅是这样吗?
以下提供两种 建树的方法。
第 1 种
对于 ,他的父亲节点是 。而父亲节点是子节点的和。只需要用所有儿子更新父亲节点。
// Θ(n) 建树
void init() {
for (int i = 1; i <= n; ++i) {
t[i] += a[i];
int j = i + lowbit(i);
if (j <= n) t[j] += t[i];
}
}
第 2 种
树状数组的性质是这样的:第 管理的区间是 。
要是我们知道数组任意区间的和就好了。所以,直接用前缀和。
void init() {
for (int i = 1; i <= n; ++i) {
t[i] = sum[i] - sum[i - lowbit(i)];
}
}
神奇的思路...
时间戳
若有多测,数组清零可能超时。
此时可用数组记录上次使用的时间,如果相同,值可用。不同说明实际的值为 。
int tag[MAXN], t[MAXN], Tag;
void reset() { ++Tag; }
void add(int k, int v) {
while (k <= n) {
if (tag[k] != Tag) t[k] = 0;
t[k] += v, tag[k] = Tag;
k += lowbit(k);
}
}
int getsum(int k) {
int ret = 0;
while (k) {
if (tag[k] == Tag) ret += t[k];
k -= lowbit(k);
}
return ret;
}
模板
以下是用结构体封装模板。
struct BIT{
int c[N],t[N];
int tg,len=N;
void clear() {tg++;}
void init(int l) {len=l;}
#define lowbit(x) ((x)&-(x))
void upd(int x,int k) {for(;x<=len;x+=lowbit(x)) if(tg==t[x]) c[x]+=k; else c[x]=k, t[x]=tg;}
int s(int x) {int p=0;for(;x;x-=lowbit(x)) if(tg==t[x]) p+=c[x]; return p;}
}c1;