树状数组拓展笔记

解决问题:给出一组序列,有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算法可以参考网站:

 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;
}


参考代码:



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值