学习笔记--分块

  • 前言

    分块是一种拓展性比较强的数据结构,对于一些难以合并的区间信息,线段树处理起来比较棘手(如区间众数),但是分块以其灵活的特点能够较快直观地处理

    本次笔记主要以hzwer的九道分块练习题与博客为主

    hzwer介绍分块的博客:http://hzwer.com/8053.html

    hzwer的分块练习题:https://loj.ac/problems/search?keyword=分块

  • 区间加法&区间求和

    应该算最为基础的一种问题模型了,用其他数据结构当然能很快地解决,不过我们用这个让大家知道分块的原理.

    首先我们要知道"分块",顾名思义,就是把一些信息分成一块一块来处理,于是对于一个区间上的信息修改或查询,这个区间可能既覆盖了一些整块,左右两边又还有一些多出来的部分,或者区间比较小被一个整块给覆盖。

    这就给我们处理的思路,对于被区间覆盖的整块,直接对这个整块的信息进行操作,两边多出来零散的部分呢就直接暴力处理。当然还有种情况就是如果区间被一个整块覆盖,也直接对区间暴力处理。这样的话设序列大小为\(N\),询问次数为\(Q\),块的大小为\(\sqrt N\),时间复杂度就为\(O((N+Q)\sqrt N)\)

    代码方面个人认为lyd的更简洁易懂,但实际上与hzwer的本质是相同的

    \(lydrainbowcat\)

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <vector>
#include <map>
#include <queue>
#define ll long long 
#define ri register int 
using namespace std;
const int maxn=50005;
const int inf=0xfffffff;
template <class T>inline void read(T &x){
	x=0;int ne=0;char c;
	while(!isdigit(c=getchar()))ne=c=='-';
	x=c-48;
	while(isdigit(c=getchar()))x=(x<<3)+(x<<1)+c-48;
	x=ne?-x:x;
	return ;
}
int n,size; 
int L[maxn],R[maxn],pos[maxn]; //L,R-每个块的左右端点;pos-元素所在块的编号 
ll sum[maxn],tag[maxn],a[maxn];//sum-块的和 tag块的标记 a-元素(序列) 注意开long long  
inline void add(int l,int r,int c){
	int p=pos[l],q=pos[r]; 
	if(p==q){
		for(ri i=l;i<=r;i++){
			a[i]+=c;
		}
		sum[p]+=(r-l+1)*c;
	}
	else{
		for(ri i=p+1;i<=q-1;i++){
			tag[i]+=c;
		}
		for(ri i=l;i<=R[p];i++)a[i]+=c;
		sum[p]+=(R[p]-l+1)*c;
		for(ri i=L[q];i<=r;i++)a[i]+=c;
		sum[q]+=(r-L[q]+1)*c;
	}
}
inline ll query(int l,int r){
	int p=pos[l],q=pos[r];
	ll ans=0;
	if(p==q){
		for(ri i=l;i<=r;i++)ans+=a[i];
		ans+=tag[p]*(r-l+1);
	}
	else{
		for(ri i=p+1;i<=q-1;i++)ans+=sum[i]+tag[i]*(R[i]-L[i]+1);
		for(ri i=l;i<=R[p];i++)ans+=a[i]+tag[p];
		for(ri i=L[q];i<=r;i++)ans+=a[i]+tag[q];
	}
	return ans;
}
int main(){
	read(n);size=sqrt(n);
	for(ri i=1;i<=n;i++){
		read(a[i]);
	}
	for(ri i=1;i<=size;i++){
		L[i]=(i-1)*size+1;   //标记每个块的左右端点位置 
		R[i]=i*size;
	}
	if(R[size]<n){size++;L[size]=R[size-1]+1,R[size]=n;}//如果没凑齐就加一个块 
	for(ri i=1;i<=size;i++){
		for(ri j=L[i];j<=R[i];j++){
			pos[j]=i;
			sum[i]+=a[j];
		}
	}
	int op,l,r,c;
	for(ri i=1;i<=n;i++){
		read(op),read(l),read(r),read(c);
		if(!op){
			add(l,r,c);
		}
		else printf("%d\n",query(l,r)%(c+1));
	}
	return 0;
}

\(hzwer\)

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <vector>
#include <map>
#include <queue>
#define ll long long 
#define ri register int 
using namespace std;
const int maxn=50005;
const int inf=0xfffffff;
template <class T>inline void read(T &x){
	x=0;int ne=0;char c;
	while(!isdigit(c=getchar()))ne=c=='-';
	x=c-48;
	while(isdigit(c=getchar()))x=(x<<3)+(x<<1)+c-48;
	x=ne?-x:x;
	return ;
}
int n; 
int blo[maxn],block;
ll sum[maxn],a[maxn],tag[maxn];//类似定义 
inline void add(int l,int r,int c){
	for(ri i=l;i<=min(blo[l]*block,r);i++){//处理最左边的块,blo[l]*block其实就是l块的右端点 
		a[i]+=c;
		sum[blo[i]]+=c;
	}
	if(blo[l]!=blo[r]){//如果 左右端点不在同一个块上 
		for(ri i=(blo[r]-1)*block+1;i<=min(r,n);i++){//处理最右块,(blo[r]-1)*block+1其实就是r块的左端点 
			a[i]+=c;
			sum[blo[i]]+=c;
		}
	}
	for(ri i=blo[l]+1;i<=blo[r]-1;i++){//处理整块 
		sum[i]+=block*c;
		tag[i]+=c;
	}
}
inline ll query(int l,int r,int p){
	ll ans=0;
	for(ri i=l;i<=min(blo[l]*block,r);i++){
		ans+=a[i]+tag[blo[i]];
	}
	if(blo[l]!=blo[r]){
		for(ri i=(blo[r]-1)*block+1;i<=min(r,n);i++){
			ans+=a[i]+tag[blo[i]];
		}
	}
	for(ri i=blo[l]+1;i<=blo[r]-1;i++){
		ans+=sum[i];
	}
	return ans;
}
int main(){
	read(n);
	block=sqrt(n);
	for(ri i=1;i<=n;i++){
		read(a[i]);
		blo[i]=(i-1)/block+1;
		sum[blo[i]]+=a[i];
	}
	int op,l,r,c;
	for(ri i=1;i<=n;i++){
		read(op),read(l),read(r),read(c);
		if(!op)add(l,r,c);
		else printf("%d\n",query(l,r,c+1)%(c+1));
	}
	return 0;
}
  • 区间加法&区间小于某数个数

    这个思路比较有意思,我们在整块中二分,这就要求整块是有序的,但左右两边散块一但暴力求改后所处的块可能就会不有序,所以要重构那两个块。用vector可以大大减少代码量,但是千万要注意tag即标记对你操作的影响,对于初学者来说非常容易出错

    比较有趣的是暴力好象比分块更快

#pragma GCC optimize(3) 
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <cmath>
#include <algorithm>
#include <cctype>
#include <vector>
#include <map>
#include <queue>
#define ri register int 
#define ll long long 
using namespace std;
const int inf=0xfffffff;
const int maxn=50005;
int a[maxn],blo[maxn],tag[maxn],block,c;
vector <int> g[maxn];
int n;
template <class T>inline void read(T &x){
	x=0;int ne=0;char c;
	while(!isdigit(c=getchar()))ne=c=='-';
	x=c-48;
	while(isdigit(c=getchar()))x=(x<<3)+(x<<1)+c-48;
	x=ne?-x:x;
	return ;
}
inline void reset_block(int now){//重构
	g[now].clear();
	for(ri i=(now-1)*block+1;i<=min(now*block,n);i++){
		g[now].push_back(a[i]);
	}
	sort(g[now].begin(),g[now].end());
}
inline void add(int l,int r){
	for(ri i=l;i<=min(blo[l]*block,r);i++){
		a[i]+=c;
	}
	reset_block(blo[l]);
	if(blo[l]!=blo[r]){
		for(ri i=(blo[r]-1)*block+1;i<=r;i++){
			a[i]+=c;
		}
		reset_block(blo[r]);
	}
	for(ri i=blo[l]+1;i<=blo[r]-1;i++){
		tag[i]+=c;
	}
}
inline int query(int l,int r,int x){
	int cnt=0;
	for(ri i=l;i<=min(blo[l]*block,r);i++){
		if(a[i]+tag[blo[i]]<x)cnt++;
	}
	if(blo[l]!=blo[r]){
		for(ri i=(blo[r]-1)*block+1;i<=min(r,n);i++){
			if(a[i]+tag[blo[i]]<x)cnt++;
		}
	}
	int tmp;
	for(ri i=blo[l]+1;i<=blo[r]-1;i++){
		tmp=lower_bound(g[i].begin(),g[i].end(),x-tag[i])-g[i].begin();
		cnt+=tmp;
	}
	return cnt;
}
int main(){
	read(n);
	block=sqrt(n);
	for(ri i=1;i<=n;i++){
		read(a[i]);
		blo[i]=(i-1)/block+1;
		g[blo[i]].push_back(a[i]);
	}
	for(ri i=1;i<=blo[n];i++){
		sort(g[i].begin(),g[i].end());
	}
	int op,l,r;
	for(ri i=1;i<=n;i++){
		read(op),read(l),read(r),read(c);
		if(!op){
			add(l,r);
		}
		else{
			printf("%d\n",query(l,r,c*c));
		}
	}
	return 0;
}
  • 区间加法&区间前驱

    \(X\)的前驱就是小于\(X\)的最大数,用上面那题类似的思路,整块中二分,左右散块修改后重构,查询时暴力查询

    然而hzwer大佬用了set,可是我已经对set的常数产生了心理阴影(相对其他STL),同时LOJ讨论区中有人说set可以被hack,感兴趣的可以看一看set写法,这里给出的还是vector二分解法,同时注意tag对操作的影响

#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <algorithm>
#include <cctype>
#include <cmath>
#include <vector>
#include <map>
#include <queue>
#define ri register int 
#define ll long long 
using namespace std;
const int inf=0xfffffff;
const int maxn=100005;
template <class T>inline void read(T &x){
	x=0;int ne=0;char c;
	while(!isdigit(c=getchar()))ne=c=='-';
	x=c-48;
	while(isdigit(c=getchar()))x=(x<<3)+(x<<1)+c-48;
	x=ne?-x:x;
	return ;
}
int n;
int blo[maxn],block,tag[maxn],a[maxn];
vector <int>g[maxn];
inline void reset_block(int now){
	g[now].clear();
	for(ri i=(now-1)*block+1;i<=min(now*block,n);i++){
		g[now].push_back(a[i]);
	}
	sort(g[now].begin(),g[now].end());
}
inline void add(int l,int r,int c){
	for(ri i=l;i<=min(blo[l]*block,r);i++){
		a[i]+=c;
	}
	reset_block(blo[l]);
	if(blo[l]!=blo[r]){
		for(ri i=(blo[r]-1)*block+1;i<=min(r,n);i++){
			a[i]+=c;
		}
		reset_block(blo[r]);
	}
	for(ri i=blo[l]+1;i<=blo[r]-1;i++){
		tag[i]+=c;
	}
}
inline int query(int l,int r,int x){
	int ans=-inf;
	for(ri i=l;i<=min(blo[l]*block,r);i++){
		if(a[i]+tag[blo[i]]<x)ans=max(a[i]+tag[blo[i]],ans);
	}
	if(blo[l]!=blo[r]){
		for(ri i=(blo[r]-1)*block+1;i<=min(r,n);i++){
			if(a[i]+tag[blo[i]]<x)ans=max(a[i]+tag[blo[i]],ans);
		}
	}
	int tmp=0;
	for(ri i=blo[l]+1;i<=blo[r]-1;i++){
		tmp=lower_bound(g[i].begin(),g[i].end(),x-tag[i])-g[i].begin();
		if(tmp!=0)ans=max(ans,g[i][tmp-1]+tag[i]);//注意这里要加上标记 
	}
	if(ans==-inf)ans=-1;
	return ans;
}
int main(){
	read(n);
	block=sqrt(n);
	for(ri i=1;i<=n;i++){
		read(a[i]);
		blo[i]=(i-1)/block+1;
		g[blo[i]].push_back(a[i]);
	}
	for(ri i=1;i<=blo[n];i++){
		sort(g[i].begin(),g[i].end());
	}
	int op,l,r,c;
	for(ri i=1;i<=n;i++){
		read(op),read(l),read(r),read(c);
		if(!op){
			add(l,r,c);
		}
		else{
			printf("%d\n",query(l,r,c));
		}
	}
	return 0;
}
posted @ 2018-06-17 23:01  Rye_Catcher  阅读(164)  评论(0编辑  收藏  举报