前言
不得不说,分块真是一种优雅的暴力。毕竟人家就是可以暴力扫+部分统计来做到根号复杂度,还能顺手解决区间众数这种线段树不好解决的问题~
博主的分块全是跟着黄学长的博客学的,所以本文不应该叫详解,顶多是我对分块的看法+我做分块九题的心得体会(毕竟学长已经写得非常好了……)。
在此还是表达一下对黄学长的敬仰和感谢%%%
(P.s:为了表现出我没有咕咕咕所以即使没有写完还是发上来了233)
1.分块の定义
给定N个数,M个操作,操作包括区间/单点加,区间/单点查询
这样的题我们见得多了,毕竟是线段树的模板题内容嘛。
但要是不用线段树呢?
我们来思考一下,线段树处理这些问题为什么那么快?
1、面对单点操作,复杂度为树高,即每次操作
2、面对区间操作,由于在线段树上一个点就对应一个区间,我们只需查询这些点,给点打标记,就可以快速处理,复杂度也近似于
我们如果用数组来近似处理的话,单点的操作都可以在O(1)的时间内做到,但是区间呢?既然线段树可以用点对应区间,我们是不是可以考虑把序列分成区间来处理?然后只需要统计一下区间信息,给区间打标记,是不是也可以快速回答上述操作?
好了,分块的基本思想就是这样:把序列划分为多段区间。如果查询范围小,我们可以直接暴力扫;当查询范围变大,包含了我们统计过的区间,我们对于零散的部分暴力统计,而被包含在查询范围内的区间,则可以用已经统计过的信息来进行回答了。
当然随便划分是不行的。经过众位神仙的推导,得出当块的大小为 时,均摊复杂度可以达到最小值。(当然某些毒瘤题的分块大小也可能是 )
P.S:不知道各位在考场上打暴力的时候有没有用过类似的思想……反正我曾经用类似的分段统计的思想+打表强行过了矩阵快速幂的题……
2.分块の操作
一般情况下,我们用数组存放原序列,用动态数组来维护每个块(其实很多时候不需要)。当然这还是因题而异。比如你可以用链表存放原序列,用平衡树维护每个块。
① 块的划分
首先是一开局就要做的事:进行块的划分和信息的统计。
我们用
存放原序列,用
表示原序列中第
位属于第
个块,
存每块的和。
是分块大小,
是序列长度:
const int N=1e6+5;
int n,v[N],bl[N],sq;
int main()
{
n=rad();sq=sqrt(n);
for(rint i=1;i<=n;i++){
val[i]=rad();
bl[i]=(i-1)/sq+1;
tot[bl[i]]+=val[i];
//i-1是为了能够把 1~sq 存到一个块
}
}
②基本操作:区间查询、区间加
(单点查值&加我就不讲了,大家都是聪明人)
秉承分块的思路:整块靠统计,部分上暴力。
两边循环统计,中间的直接调用 即可。
1、区间查询
我们对照着图来看代码。(黑色的是划分出的块)(图丑见谅)
ivoid query_some(int l,int r,int x)
{
int sum=0;
//-----处理 序号1处 的信息:暴力统计
for(rint i=l;i<=min(bl[l]*sq,r);i++)//请好好体会这里的上界
sum+=v[i];
//-----如果l和r在同一块里,此时就可以退出了
if(bl[l]!=bl[r]){
//-----处理 序号3处 的信息:暴力统计
for(rint i=(bl[r]-1)*sq+1;i<=r;i++)//同样好好体会这里的下界
sum+=v[i];
//-----处理 序号2处 的信息:直接查询
for(rint i=bl[l]+1;i<=bl[r]-1;i++)//这里的i是块的编号了
sum[i]+=tot[i];
}
cout<<ans<<endll;
}
2、区间加
然后是区间加。在这里我们延续线段树的习惯——打标记。
两边暴力,中间打标记即可。
代码:(其实长得几乎一样啊喂)
int tag_a[N];
//用一个数组来记录第 i 块的加法标记
ivoid add_some(int l,int r,int x)
{
for(rint i=l;i<=min(bl[l]*sq,r);i++)
v[i]+=x;
if(bl[l]!=bl[r]){
for(rint i=(bl[r]-1)*sq+1;i<=r;i++)
v[i]+=x;
for(rint i=bl[l]+1;i<=bl[r]-1;i++)
tot[i]+=x*sq,tag_a[i]+=x;
}
}
这个区间开方的也可以考虑一下(虽然我下面要讲),跟线段树的做法近似。
③重构相关:区间乘、区间查询前驱
将这两个操作放到这里说,主要是为了介绍分块的另一个重要函数——重构 。
某些时候我们对块进行局部修改之后,会影响信息的统计,这时候就要把整个块重构一次,顺便下传标记啊统计信息啊什么的。
1、区间乘
首先是区间乘。线段树上做乘法的时候,我们会把加法标记也做一次乘法处理,分块同理,所以就该输出(对应值 乘法表记)+加法标记
吗?并不。
当我们修改块的部分时,可能会影响到信息的正确性。举个例子:
一共九个数,a[4]=4。tag1为乘法标记,tag2为加法标记
操作1:a[2]~a[6] 乘 3
操作2:a[1]~a[4] 加 2
正确答案:a[4]=14
实际情况:
第一次操作中,a[4]所在块被打上乘3的标记,a[4]=4,tag1=3,tag2=0.
第二次操作中,a[4]+=2,a[4]=6,tag1=3,tag2=0.
输出结果:cout<< (a[4]*tag1)+tag2 输出18 WA
很明显,我们处理边上两块的时候容易出问题。那怎么办呢?重构吧。
每当我们要对块的部分进行修改,先重构这个块,把标记全部下传,再进行部分修改。这样虽然看起来暴力,但却是行之有效的方法。(且复杂度不会炸妈)(且我不会证)
代码:
//题目有要求取模,我就不删 %mod 了=
ivoid reset(int x)
{
for(rint i=(x-1)*sq+1;i<=x*sq;i++)
v[i]=(v[i]*tag_b[x]+tag_a[x])%mod;
//把整个块重构,标记全部下传
tag_b[x]=1;tag_a[x]=0;
}
//无论是加还是乘都有reset,别的和线段树无异……吧?
ivoid addsome(int l,int r,int x)
{
reset(bl[l]);
for(rint i=l;i<=min(bl[l]*sq,r);i++)
v[i]+=x,v[i]%=mod;
if(bl[l]!=bl[r]){
reset(bl[r]);
for(rint i=(bl[r]-1)*sq+1;i<=r;i++)
v[i]+=x,v[i]%=mod;
for(rint i=bl[l]+1;i<=bl[r]-1;i++)
tag_a[i]+=x,tag_a[i]%=mod;
}
}
ivoid mulsome(int l,int r,int x)
{
reset(bl[l]);
for(rint i=l;i<=min(bl[l]*sq,r);i++)
v[i]*=x,v[i]%=mod;
if(bl[l]!=bl[r]){
reset(bl[r]);
for(rint i=(bl[r]-1)*sq+1;i<=r;i++)
v[i]*=x,v[i]%=mod;
for(rint i=bl[l]+1;i<=bl[r]-1;i++)
tag_a[i]*=x,tag_b[i]*=x,
tag_a[i]%=mod,tag_b[i]%=mod;
}
}
2、区间查询前驱
你看,这个线段树就处理不了~ (权值线段树冷笑一声)
这个问题就很有意思了,毕竟我们的 是不能改变顺序的,也就意味着我们没办法在原数组上快速统计前驱。
那就没办法了吗??当然有。还记得我说过“用动态数组维护每个块”吗?
那么具体怎么做呢??
首先,我们在最开始分块时,把每个块的元素全加到对应的 里面去:
vector<int,int> block[2005];//其实sqrt(n)个块哪需要开这么大……
for(rint i=1;i<=n;i++)
{
v[i]=rad();
bl[i]=((i-1)/sq)+1;
block[bl[i]].push_back(v[i]);
}
//此题不需要统计区间和,所以去掉了tot[i]
然后,我们对每个块排序。没错,直接排序:
for(rint i=1;i<=bl[n];i++)sort(block[i].begin(),block[i].end());
那么都是排好序的了,我们查询自然就方便了:
ivoid query_pre(int a,int b,int c)//区间查询x的前驱
{
//我就不多说了,这个相信大家都能看懂
mx=-inf;
for(rint i=a;i<=min(bl[a]*sq,b);i++){
if(v[i]+tag_a[bl[a]]<c)mx=max(mx,v[i]+tag_a[bl[a]]);
}
if(bl[a]!=bl[b]){
for(rint i=(bl[b]-1)*sq+1;i<=b;i++){
if(v[i]+tag_a[bl[b]]<c)mx=max(mx,v[i]+tag_a[bl[b]]);
}
for(rint i=bl[a]+1,t;i<=bl[b]-1;i++){
t=lower_bound(block[i].begin(),block[i].end(),c-tag_a[i])-block[i].begin();
if(t>=1)
mx=max(block[i][t-1]+tag_a[i],mx);
}
}
cout<<(mx==-inf?-1:mx)<<endll;
}
emmm……这么简单?当然不是。如果我们还要同时维护区间加该怎么办呢?(笑
请务必记住:分块的本质是暴力!暴力!暴力!(优雅的
所以……
正常的区间加+块内重构就完事了!~(对就这么简单,重构就完事了)
ivoid reset(int x)//分块内部の重塑
{
block[x].clear();
for(rint i=(x-1)*sq+1;i<=min(x*sq,n);i++)
block[x].push_back(v[i]);
sort(block[x].begin(),block[x].end());
//清空——重新加进去——重新排序
//为什么不直接重排?
//因为部分修改只修改了v[i],对应block[i]里面的信息还没有改
}
ivoid addsome(int a,int b,int c)//区间加
{
for(rint i=a;i<=min(bl[a]*sq,b);i++)
v[i]+=c;
reset(bl[a]);//加完就重塑
if(bl[a]!=bl[b]){
for(rint i=(bl[b]-1)*sq+1;i<=b;i++)
v[i]+=c;
reset(bl[b]);
for(rint i=bl[a]+1;i<=bl[b]-1;i++)
tag_a[i]+=c;
}
}
③进阶操作:区间开方、区间查询某个值的个数&区间覆盖、单点插值
待更新~~~~~~~(也许要等到下周了23333