解决问题:给出一组序列,有n个,分别为a1,a2......,an,现有两个操作:
1.A i x把ai的值增加x
2.Q l r把l到r的和求出
解决方法:这是树状数组的提出的原因和基本问题,具体将在以后的基础讲解中回顾,
直接上代码:
struct Bit{
int b[maxn];
int bit(int x){
return x&(-x);
}
void add(int i,int data){
while (i<=maxn){
b[i]+=data;
i+=bit(i);
}
}
int sum(int i){
int t=0;
while (i>0){
t+=b[i];
i-=bit(i);
}
return t;
}
int query(int l,int r){
return sum(r)-sum(l);
}
}
此时如果加上两个操作:
3.C l r x把[l,r)区间的a数组全部增加x
4.QQ i求ai的值
这时,该怎么办呢,树状数组擅长于修改点查找区间,可现在是修改区间查找点?
先解决这个问题:
有n个人,来游乐园玩,时间分别为[l[i],r[i]),问哪个时刻的人最多?
这需要一个在hankcs的博客中称为imos的算法,形象的说是刷轴法。
即:
将区间全部投影到x轴上,可以发现,从l[i]开始的地方,后面的线段比较粗,而到r[i]时有突然变浅——
于是我们可以得到一个很巧妙的思路:建立一个x数组,模拟x轴,对于每个区间,进行x[l[i]]++,x[r[i]]--,的操作然后从0开始进行tmp+=x[i],打擂台求最大的tmp,
当然还可以用时间换空间,构建新的数组
b[cnt].first=l[i],b[cnt].second=1,cnt++
b[cnt].first=r[i],b[cnt].second=-1,cnt++然后用快排(注意1要排在-1前头)
从0到cnt求和,同时打擂台求最大值
给出代码方便理解:
#include<cstdio>
#include<algorithm>
#define maxn 51000
#define INF 0x7f7f7f7f
using namespace std;
struct Node{
int time,data;
bool operator < (Node b)const{
if (time!=b.time)return time<b.time;
return data>b.data;
}
}b[maxn];
int x[maxn];
int l[maxn],r[maxn];
int n,cnt=0;
int main(){
scanf("%d",&n);
for (int i=0;i<n;i++){
scanf("%d%d",&l[i],&r[i]);
x[l[i]]++;x[r[i]]--;
b[cnt].time=l[i];b[cnt].data=1;cnt++;
b[cnt].time=r[i];b[cnt].data=-1;cnt++;
}
int tmp=0,ans=INF;
for (int i=0;i<maxn;i++){
tmp+=x[i];
ans=max(ans,tmp);
}
sort(b,b+cnt);
tmp=0;int res=INF;
for (int i=0;i<cnt;i++){
tmp+=b[i].data;
res=max(res,tmp);
}
return 0;
}
那么再回到原来的问题:
3.C l r x把[l,r)区间的a数组全部增加x
基于刚才的思想:
进行两次1.A操作:A l x,A r -x
但这时,并不是区间整体加了,而仅仅是两个点而已,
但刚刚的做法还有个关键的步骤:累加
没错,这里也可以累加,这是求出1..i的和即是增加了x后的i
非常的巧妙,如果读者想继续深究imos算法可以参考网站:
对于整个拓展的程序:
#include<cstdio>
#include<algorithm>
#define maxn 51000
using namespace std;
int n;
int a[maxn];
struct Bit{
int b[maxn];
int bit(int x){
return x&(-x);
}
void add(int i,int data){
while (i<=maxn){
b[i]+=data;
i+=bit(i);
}
}
void add(int l,int r,int data){
add(l,data);
add(r,-data);
}
int sum(int i){
int t=0;
while (i>0){
t+=b[i];
i-=bit(i);
}
return t;
}
int query(int l,int r){
return sum(r)-sum(l-1);
}
int query(int i){
return sum(i);
}
int query_improve(int l,int r){
return query(r)-query(l-1);
}
}result;
int main(){
scanf("%d",&n);
for (int i=0;i<n;i++)
scanf("%d",&a[i]);
int T;
scanf("%d",&T);
while (T--){
char cmd;
scanf("\n%c",&cmd);
if (cmd=='A'){
int i,x;
scanf("%d%d",&i,&x);
i--;
result.add(i,x);
}else{
int l,r;
scanf("%d%d",&l,&r);
l--;r--;
printf("%d\n",result.query(l,r));
}
}
return 0;
}
现在我们将问题进一步加难:
1.l r x将[l,r)加x
2.l r 查询[l,r)的数和
怎么办?
沿用之前的思路。
我们再次使用imos算法,但要修改。
我们考虑,对于一次区间修改,
当i=[1..n]时,此次修改对其前缀和的贡献。
当i=[1..l-1]时,无贡献
当i=[l,r-1]时,贡献=x*(i-l+1)
当i=[r,n]时,贡献=x*(r-l+1)
可以发现,只有第二种情况比较复杂。
套路,将未知的i与已知的l分开,
可得:x*i-x*(L-1)
而查询时,我们还是使用前缀和的思想,result=query(r-1)-query(l-1)注意区间是[l,r)
也就是说我们还是需要维护前缀和,
但是由于i的未知,我们只用一个树状数组显然无法在o(logn)的时间内维护,
所以我们考虑维护两个树状数组。
设sum1[l]=sigma(x)
sum2[l]=sigma(-x*(l-1))
那么我们可以推导出query(i)=sum1[i]*i+sum2[i]
那么剩下的就是点修改与查询了。
#include<cstdio>
#include<algorithm>
using namespace std;
void add(int *a,int i,int x){
while (i<=n){
a[i]+=x;
i+=i&(-i);
}
}
int sum(int *a,int i){
int s=0;
while (i>0){
s+=a[i];
i-=i&(-i);
}
return s;
}
int main(){
init();//同上代码,在此不码
for (int i=1;i<=n;i++)
add(sum2,i,x[i]);
for (int i=1;i<=m;i++)
if (opt==1){
add(sum2,l,-x*(l-1));
add(sum1,l,x);
add(sum2,r+1,x*r);
add(sum1,r+1,-x);
}//修改
else{
LL res=0;
res+=sum(sum2,r[i])+sum(sum1,r[i])*r[i];
res-=sum(sum2,l[i]-1)+sum(sum1,l[i]-1)*(l[i]-1);
printf("%lld\n",res);
}
return 0;
}
参考代码: