分块的9题是跟着黄学长的博客写的,算是真正的入门了吧。
例题1:给出一个长为n的数列,以及n个操作,操作涉及区间加法,单点查值。
题解By hzwer:
这是一道能用许多数据结构优化的经典题,可以用于不同数据结构训练。数列分块就是把数列中每m个元素打包起来,达到优化算法的目的。以此题为例,如果我们把每m个元素分为一块,共有n/m块,每次区间加的操作会涉及O(n/m)个整块,以及区间两侧两个不完整的块中至多2m个元素。我们给每个块设置一个加法标记(就是记录这个块中元素一起加了多少),每次操作对每个整块直接O(1)标记,而不完整的块由于元素比较少,暴力修改元素的值。每次询问时返回元素的值加上其所在块的加法标记。这样每次操作的复杂度是O(n/m)+O(m),根据均值不等式,当m取√n时总复杂度最低,为了方便,我们都默认下文的分块大小为√n。
作为第一题,理解了分块的分发和基本的处理方法。
分块的方式有两种
- 块大小?固定(对于所有?都相同),取一个在sqrt(maxn)附近的值;
- 块大小?不固定,对于每个?取sqrt(n)。
第二种写法暂时没写过,习惯写第一种。
块的左端点 (bl[x]-1)*blo+1 右端点 bl[x]*blo
一般分成三种类型 [l,L) [L,R] (R,r]
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=50006;
int n,blo,a[MAXN],bl[MAXN],atag[MAXN];
void add(int l,int r,int c)
{
for(int i=l;i<=min(bl[l]*blo,r);i++) a[i]+=c;
if(bl[l]!=bl[r])
for(int i=(bl[r]-1)*blo+1;i<=r;i++) a[i]+=c;
for(int i=bl[l]+1;i<=bl[r]-1;i++)
atag[i]+=c;
}
int get(int x) {
return a[x]+atag[bl[x]];}
int main()
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
cin>>n; blo=sqrt(n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
bl[i]=(i-1)/blo+1;
}
int opt,l,r,c;
for(int i=1;i<=n;i++)
{
scanf("%d%d%d%d",&opt,&l,&r,&c);
if(opt==0) add(l,r,c);
if(opt==1) printf("%d\n",get(r));
}
return 0;
}
例题2:给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的元素个数。
题解:有了上一题的经验,我们可以发现,数列简单分块问题实际上有三项东西要我们思考:
对于每次区间操作:
1.不完整的块 的O(√n)个元素怎么处理?
2.O(√n)个 整块 怎么处理?
3.要预处理什么信息(复杂度不能超过后面的操作)?
我们先来思考只有询问操作的情况,不完整的块枚举统计即可;而要在每个整块内寻找小于一个值的元素数,于是我们不得不要求块内元素是有序的,这样就能使用二分法对块内查询,需要预处理时每块做一遍排序,复杂度O(nlogn),每次查询在√n个块内二分,以及暴力2√n个元素,总复杂度O(nlogn + n√nlog√n)。
第二题 有一个lower_bound() 的操作。
lower_bound和upper_bound其实就相当于是一个二分,在一个非递减序列中,可以O(logn)求出给出的数在序列中的位置。
分块的不完整区间的操作。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=50006;
int n,blo,a[MAXN],bl[MAXN],atag[MAXN];
vector< int > v[1000];
void rebuild(int x)
{
v[x].clear();
for(int i=(x-1)*blo+1;i<=min(n,x*blo);i++)
v[x].push_back(a[i]);
sort(v[x].begin(),v[x].end());
}
void add(int l,int r,int c)
{
for(int i=l;i<=min(bl[l]*blo,r);i++) a[i]+=c;
rebuild(bl[l]);
if(bl[l]!=bl[r])
{
for(int i=(bl[r]-1)*blo+1;i<=r;i++) a[i]+=c;
rebuild(bl[r]);
}
for(int i=bl[l]+1;i<=bl[r]-1;i++)
atag[i]+=c;
}
int get(int l,int r,int c)
{
int J=0;
for(int i=l;i<=min(bl[l]*blo,r);i++)
if(a[i]+atag[bl[i]]<c) J++;
if(bl[l]!=bl[r])
for(int i=(bl[r]-1)*blo+1;i<=r;i++)
if(a[i]+atag[bl[i]]<c) J++;
for(int i=bl[l]+1;i<=bl[r]-1;i++)
{
int C=c-atag[i];
J+=lower_bound(v[i].begin(),v[i].end(),C)-v[i].begin();
}
return J;
}
int main()
{
freopen("a.in","r",stdin);
freopen("a.out","w",stdout);
cin>>n; blo=sqrt(n);
for(int i=1;i<=n;i++)
{
scanf("%d",&a[i]);
bl[i]=(i-1)/blo+1;
v[bl[i]].push_back(a[i]);
}
for(int i=1;i<=bl[n];i++) sort(v[i].begin(),v[i].end());
int opt,l,r,c;
for(int i=1;i<=n;i++)
{
scanf("%d%d%d%d",&opt,&l,&r,&c);
if(opt==0) add(l,r,c);
if(opt==1) printf("%d\n",get(l,r,c*c));
}
return 0;
}
例题3:给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。
题解:接着第二题的解法,其实只要把块内查询的二分稍作修改即可。
不过这题其实想表达:可以在块内维护其它结构使其更具有拓展性,比如放一个 set ,这样如果还有插入、删除元素的操作,会更加的方便。
分块调试技巧:可以生成一些大数据,然后用两份分块大小不同的代码来对拍,还可以根据运行时间尝试调整分块大小,减小常数。
其实写分块的时候顺带写了暴力,只要把块的大小改为1,两个代码对拍即可。
#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int MAXN=500050;
int n,blo,a[MAXN],bl[MAXN],atag[MAXN];
vector< int > v[10000];
void rebuild(