前言
分块是一种拓展性比较强的数据结构,对于一些难以合并的区间信息,线段树处理起来比较棘手(如区间众数),但是分块以其灵活的特点能够较快且直观地处理
本次笔记主要以hzwer的九道分块练习题与博客为主
hzwer介绍分块的博客:http://hzwer.com/8053.html
hzwer的分块练习题:https://loj.ac/problems/search?keyword=%E5%88%86%E5%9D%97
区间加法&区间求和
应该算最为基础的一种问题模型了,用其他数据结构当然能很快地解决,不过我们用这个让大家知道分块的原理.
首先我们要知道"分块",顾名思义,就是把一些信息分成一块一块来处理,于是对于一个区间上的信息修改或查询,这个区间可能既覆盖了一些整块,左右两边又还有一些多出来的部分,或者区间比较小被一个整块给覆盖。
这就给我们处理的思路,对于被区间覆盖的整块,直接对这个整块的信息进行操作,两边多出来零散的部分呢就直接暴力处理。当然还有种情况就是如果区间被一个整块覆盖,也直接对区间暴力处理。这样的话设序列大小为\(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;
}