树状数组:顾名思义,就是用数组来模拟树形结构。那么衍生出一个问题,为什么不直接建树?答案是没必要,因为树状数组能处理的问题就没必要建树。
树状数组能解决什么问题:可以解决大部分基于区间上的更新以及求和问题。
1.快速求前缀和O(logn) 2.修改某一个数O(logn)
如果用普通的数组存每个数 1操作就是O(n) 2操作是O(1)的
如果维护前缀和数组 那么1操作是O(1) 2操作是O(n)
扩展:联系差分和推公式
差分的扩展:树状数组原来的操作1.快速求前缀和O(logn) 2.修改某一个数O(logn)
现在反过来1.修改某一段区间 2.求某个值 这就联系差分了
与线段树的联系:树状数组可以解决的问题都可以用线段树解决,这两者的区别在哪里呢?树状数组的系数要少很多,就比如字符串模拟大数可以解决大数问题,也可以解决1+1的问题,但没人会在1+1的问题上用大数模拟。(打小兵用大刀)
基本原理:
首先先定义a[i]为每个元素的值
然后假定我们要求的是1~x的总和 也就是前x个数的和
k>k-1>k-2>......>i
将x用二进制表示 x就可以等于2^k+2^(k-1)+2^(k-2)+.....+2^i 那么我们是不是可以求出前2^k个数 然后再去掉2^k个数 再取去掉2^k个数的前2^(k-1)个数 一直循环
example: ()为多少进制
12(10)=1100(2)=1000(2)+100(2)=8(10)+4(10)12(10)=1100(2)=1000(2)+100(2)=8(10)+4(10)
是不是先取了前8个数(1-8) 再去除前八个数 选前四个数(9-12)
那么我们现在将前x个元素分为很多个区间去求和
1.( x-2^i ,x] 里面有 2^i 个元素
2.(x-2^(i)-2^(i+1),x-2^(i) ] 里面有2^(i+1)个元素
....
3.( 0 , x - 2^i - 2^(i+1)- ..... - 2^(k-1) ] 里面有 2^(k)个元素
然后可以发现区间( L,R ] 的长度一定是R二进制表示的最后一位 1
这时候我们就可以定义一个C数组
C数组的定义是 以 x 为右端点的长度为lowbit(x)的区间等价于以x结尾 长度为2^k( 末尾有k个0等价于lowbit(x) )的区间和
那么C[x] = a[ x-lowbit(x)+1 到 x ]的和
然后这段区间和一定包含 a[ x ] 那么 C[x] = a[x] +a[x - lowbit(x) +1 到 x-1 ]的和
由于x的形式是......01000这种形式 那么x-1就是......00111
那么那么我们现在就可以继续分段了
1. (....00110 到 ...00111] 这个是C[x-1]
2. (....00100 到 ...00110] 这个是C[(x-1)-lowbit(x-1)]
3. (....00000 到 ...00100] 这个是C[(x-1)-lowbit(x-1)-lowbit[(x-1)-lowbit[x-1]]]
那么把这几个全部加起来就是C[x]了
那么更新操作怎么办呢?
我要修改a[x] 那么我就得修改包含x的c数组
假设x为......01100....那么父节点就是......10000 那么修改就是每次加lowbit
1. 楼兰图腾
tr表示这个位置有多少个这个数
#include <bits/stdc++.h>
using namespace std;
const int N=200010;
int n,m;
int tr[N];
int a[N];
int great[N],low[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;
}
}
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);
low[i]=sum(y-1);
add(y,1);
}
memset(tr,0,sizeof tr);
long long res1=0,res2=0;
for(int i=n;i;i--)
{
int y=a[i];
res1+=great[i]*(long long)(sum(n)-sum(y));
res2+=low[i]*(long long)(sum(y-1));
add(y,1);
}
printf("%lld %lld\n",res1,res2);
return 0;
}
2.一个简单的整数问题(差分)
把tr数组当成差分数组即可
假如[l,r]+c,则b[l]+=c,b[r+1]-=c;
假如求x的数是多少,相当于求b[x]的前缀和,然后可以直接用树状数组求用sum(x)即可
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=100010;
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=x;i<=n;i+=lowbit(i))
tr[i]+=c;
}
ll sum(int x)
{
ll res=0;
for(int i=x;i;i-=lowbit(i))
{
res+=tr[i];
}
return res;
}
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++)
add(i,a[i]-a[i-1]);
while(m--)
{
char op[2];
int l,r,d;
scanf("%s%d",op,&l);
if(op[0]=='C')
{
scanf("%d%d",&r,&d);
add(l,d),add(r+1,-d);
}
else
{
printf("%lld\n",sum(l));
}
}
return 0;
}
3.一个简单的整数问题2(差分+分析)
#include <bits/stdc++.h>
using namespace std;
const int N=100010;
typedef long long ll;
int n,m;
ll tr1[N];
ll tr2[N];
int a[N];
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;
}
}
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++)
{
add(tr1,i,a[i]-a[i-1]);
add(tr2,i,(ll)(a[i]-a[i-1])*i);
}
while(m--)
{
char op[2];
int l,r,d;
scanf("%s%d%d",op,&l,&r);
if(op[0]=='Q')
{
printf("%lld\n",prefix_sum(r)-prefix_sum(l-1));
}
else
{
scanf("%d",&d);
add(tr1,l,d),add(tr2,l,l*d);
add(tr1,r+1,-d),add(tr2,r+1,(r+1)*(-d));
}
}
return 0;
}
4.谜一样的牛(二分查找)
思路:从n往1看,每次给剩余的数里面第a[i]+1小的数,然后把这个数删掉,一直这样操作即可
找第a[i]+1小的数:
1.把每个数初始值为1,表明这个数还没用过,每个前缀和表明了当前数是第几小的数
2.二分查找第a[i]+1小的数,因为每个数的前缀和是递增的可以二分
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+10;
int n;
int a[N],tr[N],ans[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;
}
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",&a[i]);
for(int i=1;i<=n;i++) add(i,1);//初始化初始值,让每个数都为1,表明没有用过
for(int i=n;i;i--)
{
int b=a[i]+1;//找第a[i]+1小的数
int l=1,r=n;
while(l<r)//二分
{
int mid=l+r>>1;
if(sum(mid)>=b) r=mid;//假如前缀和大了,说明当前数不是第b小的数
else l=mid+1;
}
ans[i]=l;//让答案等于当前找到的第b小的数
add(l,-1);//把当前数删除,因为已经用过了
}
for(int i=1;i<=n;i++) printf("%d\n",ans[i]);
return 0;
}