【详解?】分块 【未完】

前言

不得不说,分块真是一种优雅的暴力。毕竟人家就是可以暴力扫+部分统计来做到根号复杂度,还能顺手解决区间众数这种线段树不好解决的问题~

博主的分块全是跟着黄学长的博客学的,所以本文不应该叫详解,顶多是我对分块的看法+我做分块九题的心得体会(毕竟学长已经写得非常好了……)。

在此还是表达一下对黄学长的敬仰和感谢%%%

(P.s:为了表现出我没有咕咕咕所以即使没有写完还是发上来了233)

1.分块の定义

给定N个数,M个操作,操作包括区间/单点加,区间/单点查询

这样的题我们见得多了,毕竟是线段树的模板题内容嘛。

但要是不用线段树呢?

我们来思考一下,线段树处理这些问题为什么那么快?

1、面对单点操作,复杂度为树高,即每次操作 O ( l o g N ) O(logN)

2、面对区间操作,由于在线段树上一个点就对应一个区间,我们只需查询这些点,给点打标记,就可以快速处理,复杂度也近似于 O ( l o g N ) O(logN)

我们如果用数组来近似处理的话,单点的操作都可以在O(1)的时间内做到,但是区间呢?既然线段树可以用点对应区间,我们是不是可以考虑把序列分成区间来处理?然后只需要统计一下区间信息,给区间打标记,是不是也可以快速回答上述操作?

好了,分块的基本思想就是这样:把序列划分为多段区间。如果查询范围小,我们可以直接暴力扫;当查询范围变大,包含了我们统计过的区间,我们对于零散的部分暴力统计,而被包含在查询范围内的区间,则可以用已经统计过的信息来进行回答了。

当然随便划分是不行的。经过众位神仙的推导,得出当块的大小为 n \sqrt n 时,均摊复杂度可以达到最小值。(当然某些毒瘤题的分块大小也可能是 n 3 \sqrt[3]{n}

P.S:不知道各位在考场上打暴力的时候有没有用过类似的思想……反正我曾经用类似的分段统计的思想+打表强行过了矩阵快速幂的题……

2.分块の操作

一般情况下,我们用数组存放原序列,用动态数组来维护每个块(其实很多时候不需要)。当然这还是因题而异。比如你可以用链表存放原序列,用平衡树维护每个块。

① 块的划分

首先是一开局就要做的事:进行块的划分和信息的统计。
我们用 v [ i ] v[i] 存放原序列,用 b l [ i ] bl[i] 表示原序列中第 i i 位属于第 b [ i ] b[i] 个块, t o t [ i ] tot[i] 存每块的和。
s q sq 是分块大小, n n 是序列长度:

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 存到一个块
	}
}
②基本操作:区间查询、区间加

(单点查值&加我就不讲了,大家都是聪明人)

秉承分块的思路:整块靠统计,部分上暴力。

两边循环统计,中间的直接调用 t o t [ i ] tot[i] 即可。

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;
	}
}

那么现在你可以做 了。

这个区间开方的也可以考虑一下(虽然我下面要讲),跟线段树的做法近似。

③重构相关:区间乘、区间查询前驱

将这两个操作放到这里说,主要是为了介绍分块的另一个重要函数——重构 r e s e t reset

某些时候我们对块进行局部修改之后,会影响信息的统计,这时候就要把整个块重构一次,顺便下传标记啊统计信息啊什么的。

1、区间乘

首先是区间乘。线段树上做乘法的时候,我们会把加法标记也做一次乘法处理,分块同理,所以就该输出(对应值 × \times 乘法表记)+加法标记

吗?并不。

当我们修改块的部分时,可能会影响到信息的正确性。举个例子:

一共九个数,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、区间查询前驱

你看,这个线段树就处理不了~ (权值线段树冷笑一声)

这个问题就很有意思了,毕竟我们的 v [ i ] v[i] 是不能改变顺序的,也就意味着我们没办法在原数组上快速统计前驱。

那就没办法了吗??当然有。还记得我说过“用动态数组维护每个块”吗?

那么具体怎么做呢??

首先,我们在最开始分块时,把每个块的元素全加到对应的 v e c vec 里面去:

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

发布了44 篇原创文章 · 获赞 16 · 访问量 7257

猜你喜欢

转载自blog.csdn.net/Cyan_rose/article/details/89053926