思想归纳

1:当急需一个数据结构,可以在无序的数列中查询第一个大于等于(或其他)一个数值(或其他)的元素,可以用线段树实现(^-^)。假如把序列分成两个区域P1和P2,那么如果存在一个大于等于x的元素,则这个元素一定会出现在P1或者P2中,线段树的工作就是查询P1和P2的最大值,如果P1的最大值大于等于x,那么就可以缩小搜索的范围,就在P1中找就行了,同理,如果P1的最大值小于x,P2的最大值大于等于x,那么就在P2中找。以此类推,不断地缩小搜索范围,就可以找出元素的下标。查询复杂度O(log(n)),建树复杂度O(n)。mx[p]存的是区间p的最大值。

 

void build(int p=1,int l=1,int r=n){
	if(l==r){mx[p]=A[l];return;}
	int mid=l+r>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	mx[p]=max(mx[p<<1],mx[p<<1|1]);
}
void query(int x,int p=1,int l=1,int r=n){
	if(l==r)return l;
	int mid=l+r>>1,ans=-1;
	if(mx[p<<1]>=x)ans=query(x,p<<1,l,mid);
	else if(mx[p<<1|1]>=x)ans=query(x,p<<1|1,mid+1,r);
	return ans;
}

 

上面的线段树只能在[1,n]的范围内寻找第一个满足某条件的下标,当如果要在一段区间[a,b]内寻找第一个满足条件的下标时,那么就要升级一下线段树,mx[p]存的p区间最大值的下标。

 

void build(int p=1,int l=1,int r=n){
	if(l==r){mx[p]=l;return;}
	int mid=l+r>>1;
	build(p<<1,l,mid);
	build(p<<1|1,mid+1,r);
	mx[p]=(A[mx[p<<1]]>=A[mx[p<<1|1]])?mx[p<<1]:mx[p<<1|1];
}
int query(int a,int b,int x,int p=1,int l=1,int r=n){
	if(l>b||r<a)return 0;
	if(l==r)return l;
	int mid=l+r>>1,ans=0;
	if(mx[p<<1]>=x)ans=query(a,b,x,p<<1,l,mid);
	if(!ans&&mx[p<<1|1]>=x)ans=query(a,b,x,p<<1|1,mid+1,r);
	return ans;
}

2:如果一个问题的点,满足在某种条件下进行跳跃,每个点跳跃一次后都能转换成一个固定的点,并且当问题需要知道一个点跳跃多次后的点时,可以用倍增法进行点的快速跳跃。用F[i][j]来表示j这个点跳2^i步后的点,所以只需求出每个点的F[0][j],就可以预处理出每个点跳2^i后的点。

for(int i=1;i<S;i++)for(int j=1;j<=n;j++)F[i][j]=F[i-1][F[i-1][j]];//预处理

当我们要跳step步时,可以用二进制判断step&(1<<i)是否为真,若为真则让这个点跳跃2^i步,就令x=F[i][x]。

for(int i=0;i<S;i++)if(step&(1<<i))x=F[i][x];//运用

也可以用位运算更快速地求出跳step步后的点。

 

while(step){//效率更高地运用
	int op=step&-step;
	op=mp[op];
	x=F[op][x];
	step^=op;
}

3:当一个问题中需要用到一种需要修改的前缀和时,可以用树状数组来维护。可以说树状数组是可以单点修改的前缀和,那么当遇到需要修改的前缀和时,就能用树状数组实现log(n)的修改,log(n)的查询。树状数组上的每个节点存的不是一个点的信息而是一段区间的信息,把数组的下标转换为二进制数来表示,一个下标管理的区间长度是2^(二进制数的末尾0的个数),例如1000管理的长度是8,10110管理的长度是2,对于一个下标i,i管理的区间是i-2^(二进制数i末尾0的个数)+1到i,这样就可以实现高效的修改与查询,修改前缀和时每次让i+=i&-i,查询时让i-=i&-i,因此查询与修改都最多在log(n)的时间内完成,这样树状数组的速度就会比线段树更快(但是树状数组能够实现的线段树一定能够实现...)。

 

void update(int *A,int i,int x){//将数组下标从i开始的数组下标都加上x
	while(i<=n){
		A[i]+=x;
		i+=i&-i;
	}
}
int query(int *A,int i){//查询下标为[1,i]区间内的数组权值之和
	int res=0;
	while(i){
		res+=A[i];
		i-=i&-i;
	}return res;
}

同时,树状数组也可以实现区间修改以及区间查询。与线段树不同的是,线段树的区间查询需要稍微修改数据结构,而树状数组的区间查询只需换一种使用的方法,就可以实现区间的修改。当我们需修改[L,R]区间内的前缀和数组时,分别对[1,L-1],[L,R],[R+1,n]区间进行分析:

(1)在[1,L-1]区间内的前缀和不变。

(2)在[L,R]区间内下标为i的数组,前缀和的增加量=(i-l+1)*x=i*x-(l-1)*x

(3)在[R+1,n]区间内的所有数组,前缀和的增加量=(r-l+1)*x=r*x-(l-1)*x;

在(2)中,由于[L,R]中的i不断在变,所以不能够处理出所有的i的前缀和变化量。于是可以用bit0和bit1分别表示前缀和变化量的随i变化而变化的量不变的量。由(2)和(3)推出的公式说明查询i时只要query(bit1,i)*i+query(bit0,i)就行了。

 

/*查询*/ 
ll res=0;
res+=query(bit0,R)+query(bit1,R)*R;
res-=query(bit0,L-1)+query(bit1,L-1)*(L-1);
cout<<res<<endl;
/*修改*/ 
add(bit0,L,-x*(L-1));
add(bit0,R+1,x*R);
add(bit1,L,x);
add(bit1,R+1,-x);

4:如果一个查询类的问题,对于每次查询,都要二分一个权值,然后check()一下该值是否可行,如果这样就可以考虑一下是否能将查询进行离线,然后把二分写在外面(比如循环16次),让check和查询数组的更新同时进行,就可以大大地缩小算法的复杂度。对于每次的循环,都将查询数组的mid进行排序,对于每次check(i),都将mid[j]==i的询问更新一下L,R,mid和ans,然后j++,如果L已经大于R就直接跳过当前询问。这样check(i)的i和查询数组的j同时在变,一次循环的复杂度就为O(n+m)。

  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值