分块

分块

特点:一种优雅的暴力,大段维护,小段朴素。

假设我们有一个长度为 \(n\) 的数组 \(a\)。

需要我们维护区间修改和区间查询等操作。

那么朴素算法就不用说了,如果是万能的线段树还行。但是线段树码量过大,容易出bug。

这个时候就得用到分块的思想。

分块思想:

对于需要维护的数组 \(a\),将其分为 \(num\) 块,每一块块长为 \(block\),然后对每一块进行维护,最后求解。

那么这个 \(block\) 是多少好呢?

根据均值不等式,当 \(\frac{n}{block}=block\),即当 \(block=\sqrt{n}\) 时最优,单次修改为 \(O(\sqrt{n})\)。

显然有些情况下,\(n\mod block \neq 0\),即分完会有剩下的元素。那么很显然可以将块数 \(+1\),就当作是新建一块。

对于每个块的维护

可以建立一个结构体来存放每一个快。

内存块的左端点、右端点、区间和。如果题目中有区间修改,那么类似的,放一个懒标记进去,表示该区间所有数都要加上多少。

对于第 \(i\) 块 \((\forall\ 1\le i\le num)\):

左端点 \(=(i-1)\lfloor{\sqrt{n}}\rfloor+1\),右端点是 \(\min (i\lfloor{\sqrt{n}}\rfloor,n)\)。因为有可能 \(n\mod block \neq 0\)。

下面就是一个例子:

其中:

\[(1\le i \le n) \\ l_i=\left\{1,5,9,13,17\right\} \\ r_i=\left\{4,8,12,16,17\right\} \]

对于每个下标 \(i\),用 \(belong_i\) 表示第 \(i\) 号元素属于第 \(belong_i\) 块。

操作维护

设 \(l,r\) 分别是增加区间的左右端点,增加 \(d\)。

若 \(belong_l=belong_r\),即在同一个块中:

将所有的 \(a_i\) 加上 \(d\),再将 \(sum_i\leftarrow sum_i+d(r-l+1)\)。(小段朴素

否则,设 \(belong_l=q,belong_r=p\):

对于区间 \([l,r_q]\)​​,用同一个块中的方法进行维护。

即维护下图的区间 \([2,4]\)。

同理,也可以维护区间 \([l_p,r]\),即上图区间 \([13,14]\)。

现在我们已经解决了区间 \([2,4]\) 和 \([13,14]\),还剩下在两个区间中间的区间 \([5,8]\) 和 \([9,12]\)。

显然 \([5,8]\) 和 \([9,12]\) 是两个块,就将块的懒标记全部加上 \(d\) 即可。

若不用懒标记,像小区间那样的朴素维护,很容易时间就超了。

代码部分

例题:P3374 【模板】树状数组 1

一个块的元素:

cpp 复制代码
struct NODE{
		ljl l,r,sum;
	}node[N];

建立块:

cpp 复制代码
void build()
	{
		block=floor(sqrt(n));
		num=n/block;
		if(n%block!=0) ++num;//不能分为整块
		for(ljl i=1;i<=num;++i)
		{
			node[i].l=(i-1)*block+1,node[i].r=min(i*block,n);//公式已说
			for(ljl j=node[i].l;j<=node[i].r;++j)
				node[i].sum+=a[j];
		}
//		for(ljl i=1;i<=num;++i)
//			cout<<node[i].l<<' '<<node[i].r<<" "<<node[i].sum<<'\n';
		for(ljl i=1;i<=n;++i)
			belong[i]=(i-1)/block+1;
		return;
	}

单点修改:

cpp 复制代码
void update(ljl x,ljl y)
	{
		a[x]+=y;
		node[belong[x]].sum+=y;
		return;
	}

区间查询:

cpp 复制代码
ljl query(ljl l,ljl r)
	{
		ljl q=belong[l],p=belong[r];
		if(q==p)
		{
			ljl ans=0;
			for(ljl i=l;i<=r;++i)
				ans+=a[i];
			return ans;
		}
		ljl ans=0;
		for(ljl i=l;i<=node[q].r;++i)
			ans+=a[i];
		for(ljl i=q+1;i<=p-1;++i)
			ans+=node[i].sum;
		for(ljl i=node[p].l;i<=r;++i)
			ans+=a[i];
		return ans;
	}

完整代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ljl long long
const ljl N=5e5+5;
ljl n,m,a[N],block;
namespace BLOCK{
	struct NODE{
		ljl l,r,sum;
	}node[N];
	ljl belong[N],num;
	void build()
	{
		block=floor(sqrt(n));
		num=n/block;
		if(n%block!=0) ++num;
		for(ljl i=1;i<=num;++i)
		{
			node[i].l=(i-1)*block+1,node[i].r=min(i*block,n);
			for(ljl j=node[i].l;j<=node[i].r;++j)
				node[i].sum+=a[j];
		}
//		for(ljl i=1;i<=num;++i)
//			cout<<node[i].l<<' '<<node[i].r<<" "<<node[i].sum<<'\n';
		for(ljl i=1;i<=n;++i)
			belong[i]=(i-1)/block+1;
		return;
	}
	void update(ljl x,ljl y)
	{
		a[x]+=y;
		node[belong[x]].sum+=y;
		return;
	}
	ljl query(ljl l,ljl r)
	{
		ljl q=belong[l],p=belong[r];
		if(q==p)
		{
			ljl ans=0;
			for(ljl i=l;i<=r;++i)
				ans+=a[i];
			return ans;
		}
		ljl ans=0;
		for(ljl i=l;i<=node[q].r;++i)
			ans+=a[i];
		for(ljl i=q+1;i<=p-1;++i)
			ans+=node[i].sum;
		for(ljl i=node[p].l;i<=r;++i)
			ans+=a[i];
		return ans;
	}
}
using namespace BLOCK;
int main(){
	ios::sync_with_stdio(0);
	cin>>n>>m;
	for(ljl i=1;i<=n;++i)
		cin>>a[i];
	build();
//	for(ljl i=1;i<=n;++i)
//		cout<<belong[i]<<' ';
//	cout<<"\n";
	while(m--)
	{
		ljl op,x,y;
		cin>>op>>x>>y;
		if(op==1)
			update(x,y);
		else
		{
			cout<<query(x,y)<<'\n';
		}
//		cout<<"--------------\n";
//		for(ljl i=1;i<=num;++i)
//			cout<<node[i].l<<" "<<node[i].r<<" "<<node[i].sum<<'\n';
//		cout<<"----------------\n";
	}
	return 0;
}

例题二:P3372 【模板】线段树 1

因为上一题仅仅是单点修改,过于简单。

所以上强度:

本题需要区间修改和区间查询,具体看代码:

cpp 复制代码
#include<bits/stdc++.h>
using namespace std;
#define ljl long long
const ljl N=1e5+5,BLOCKS=1e3+5;
ljl n,a[N],q,l,r,v;
namespace BLOCK{
	struct NODE{
		ljl l,r,sum,lz;
	}node[BLOCKS];
	ljl belong[N],num,block;
	void build()//建块
	{
		block=(ljl)sqrt(n);
		num=n/block;
		if(n%block)++num;
		for(ljl i=1;i<=num;++i)
		{
			node[i].l=(i-1)*block+1,node[i].r=min(i*block,n);
			for(ljl j=node[i].l;j<=node[i].r;++j)
				node[i].sum+=a[j];
		}
		for(ljl i=1;i<=n;++i)
			belong[i]=(i-1)/block+1;
		return;
	}
	void change_part(ljl id,ljl l,ljl r)//对于是一个块内的区间修改
	{
		for(ljl i=l;i<=r;++i)
			a[i]+=v;
		node[id].sum+=v*(r-l+1);
	}
	void change(ljl l,ljl r)//询问的区间修改
	{
		ljl q=belong[l],p=belong[r];
		if(q==p)
		{
			change_part(q,l,r);return;
		}
		change_part(q,l,node[q].r);//按照上文讲过的方法实现即可
		change_part(p,node[p].l,r);
		for(ljl i=q+1;i<p;++i)
			node[i].lz+=v;//这里是懒标记
		return;
	}
	ljl query_part(ljl id,ljl l,ljl r)
	{
		ljl ans=0;
		for(ljl i=l;i<=r;++i)
			ans=ans+(a[i]+node[id].lz);
		return ans;
	}
	ljl query(ljl l,ljl r)
	{
		ljl q=belong[l],p=belong[r];
		if(q==p)
			return query_part(q,l,r);
		ljl ans=0;
		ans+=query_part(q,l,node[q].r);
		ans+=query_part(p,node[p].l,r);
		for(ljl i=q+1;i<p;++i)
			ans=ans+node[i].sum+(node[i].r-node[i].l+1)*node[i].lz;
		return ans;		
	}
}
using namespace BLOCK;
int main(){
	ios::sync_with_stdio(0);
	cin>>n>>q;
	for(ljl i=1;i<=n;++i)
		cin>>a[i];
	build();
	while(q--)
	{
		ljl op;
		cin>>op;
		if(op==1)
		{
			cin>>l>>r>>v;
			change(l,r);
		}
		else
		{
			cin>>l>>r;
			cout<<query(l,r)<<'\n';
		}
	}
	return 0;
}

线段树和分块的比较:

线段树1:

时间 空间 代码长度
分块 974ms 1.98MB 1.48KB
线段树 297ms 11.19MB 1.67KB

树状数组1:

时间 空间 代码长度
分块 1.34s 9.55MB 1.40KB
树状数组 521ms 4.23MB 554B

虽然分块不如线段树或树状数组,但是在不熟练他们俩的情况下可以使用分块,反正能A。