分块入门

基本思想

把一个需要操作的序列分成若干块,分别处理,从而优化时间复杂度。

容易证明块长为 \(\sqrt n\) 时复杂度最优。

分块常规单次操作复杂度为 \(\mathcal{O}(\sqrt n)\),一般可以当做 \(\mathcal{O}(\log^2n)\) 来计算复杂度。

接下来给几道例题。

T1

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,单点查值。

就当模板放了。

点击查看代码
void add(int l,int r,int c)
{
	int sid=id[l],eid=id[r];
	if(sid==eid)
	{
		for(int i=l;i<=r;i++)
		a[i]+=c;
		return ;
	}
	for(int i=l;id[i]==sid;i++)a[i]+=c;
	for(int i=sid+1;i<eid;i++)tag[i]+=c;
	for(int i=r;id[i]==eid;i--)a[i]+=c;
}

T2

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的元素个数。

对于区间加法,类似线段树设置懒标记,对于查询操作,可以用二分解决。我们每次区间加法之后对所有涉及到的不完整区间重新排序,保证可以二分找到给定值,对于完整区间,由于顺序不变,则更新 \(tag\) 值即可。

具体的,我们对于每一个块,用一个 \(vector\) 记录值,每次排序对 \(vector\) 排序,可以证明复杂度正确。

放一下查询和更新的代码:

点击查看代码
void work(int x)
{
	v[id[x]].clear();
	for(int i=(id[x]-1)*len+1;i<=min(n,id[x]*len);i++)
	v[id[x]].push_back(a[i]);
	sort(v[id[x]].begin(),v[id[x]].end());
}
int ask(int l,int r,int c)
{
	int sid=id[l],eid=id[r],ans=0;
	if(sid==eid)
	{
		for(int i=l;i<=r;i++)if(a[i]+tag[id[l]]<c)ans++;
		return ans;
	}
	for(int i=l;id[i]==sid;i++)if(a[i]+tag[id[l]]<c)ans++;
	for(int i=sid+1;i<eid;i++)ans+=lower_bound(v[i].begin(),v[i].end(),c-tag[i])-v[i].begin();
	for(int i=r;id[i]==eid;i--)if(a[i]+tag[id[r]]<c)ans++;
	return ans;
}

T3

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,询问区间内小于某个值 \(x\) 的前驱(比其小的最大元素)。

和上一道题很类似,只是对于查询操作,我们二分找到每个块里面符合条件的最大值,取总最大值即可。

查询部分:

点击查看代码
int sid=id[l],eid=id[r],ans=-1;
if(sid==eid)
{
	for(int i=l;i<=r;i++)if(a[i]+tag[id[l]]<c)ans=max(ans,a[i]+tag[id[l]]);
	return ans;
}
for(int i=l;id[i]==sid;i++)if(a[i]+tag[id[l]]<c)ans=max(ans,a[i]+tag[id[l]]);
for(int i=sid+1;i<eid;i++)
{
	int x=c-tag[i];
	int y=lower_bound(v[i].begin(),v[i].end(),x)-v[i].begin();
	if(--y>=0)
		ans=max(ans,v[i][y]+tag[i]);
}
for(int i=r;id[i]==eid;i--)if(a[i]+tag[id[r]]<c)ans=max(ans,a[i]+tag[id[r]]);
return ans;

T4

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间加法,区间求和。

简单,模拟线段树的思路,记录区间懒标记 \(tag\),修改的时候对于完整区间只更新 \(tag\) 值,查询的时候对于不完整的块,让每个元素加上他所在区间的 \(tag\) 值累加,对于完整的块,则让区间和加上 \(tag\times len\)(\(len\) 为块长)后累加答案即可。

太简单,代码不放了。

T5

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间开方,区间求和。

势能分析,由于 \(\sqrt 1=1\),所以我们用一个布尔数组 \(f\) 表示当前块是否已经全部小于等于 \(1\),每次修改的时候对于 \(f_i\le 1\) 的块跳过,否则暴力修改即可。

整块修改部分代码:

点击查看代码
for(int i=sid+1;i<eid;i++)
	if(!f[i]) 
	{
		f[i]=1;
		for(int j=(i-1)*len+1;j<=min(n,i*len);j++) 
		{
			tag[i]=tag[i]-a[j]+sqrt(a[j]);
			a[j]=sqrt(a[j]);
			if(a[j]!=0&&a[j]!=1)
				f[i]=0;
		}
	}

T6

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及单点插入,单点询问。

其实稍微想一想就发现,我们只需要维护每一个块的内部排列即可,对于每个修改操作,在块内正常暴力修改,对于每个查询操作,从 \(1\) 开始跑,每次减去当前块长,最后直接输出即可。

放一下代码:

点击查看代码
void add(int x,int k)
{
	int sum=0;
	for(int i=1;i;i++)
	{
		if(x<v[i].size())
		{
			v[i].push_back(maxn);
			for(int j=v[i].size()-1;j>x;j--)
			{
				v[i][j]=v[i][j-1];	
			}
			v[i][x]=k;
			return ;
		}
		x-=v[i].size();
	}
}
void ask(int x)
{
	int sum=0;
	for(int i=1;;i++) 
	{
		if(x<v[i].size()) {cout<<v[i][x]<<'\n';return ;}
		x-=v[i].size();
	}
}

T7

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间乘法,区间加法,单点询问。

左转线段树,模拟懒标记就行了。有点难写,所以代码不放了。

T8

给出一个长为 \(n\) 的数列,以及 \(n\) 个操作,操作涉及区间询问等于一个数 \(c\) 的元素,并将这个区间的所有元素改为 \(c\)

我们对每个区间记录一个 \(tag\),代表是否被全部赋值并且赋值成了什么。每次操作的时候先对左右两个不完整的区间根据 \(tag\) 重新赋值,并更新 \(tag\),然后对于中间的区间,如果该区间 \(tag\) 有值,累加答案并修改区间 \(tag\),否则暴力修改,复杂度可以证明是正确的。

放一下区间修改的代码:

点击查看代码
for(int i=sid+1;i<eid;i++)
	if(tag[i]!=-1)
	{
		if(tag[i]!=c)tag[i]=c;
		else ans+=len;
	}
	else
	{
		for(int j=(i-1)*len+1;j<=i*len;j++)
		{
			if(a[j]!=c)a[j]=c;
			else ans++;
		}
		tag[i]=c;
	} 

分块是一种很有用的算法,希望大家勤加练习。

posted @ 2024-07-17 21:35  Redamancy_Lydic  阅读(5)  评论(0编辑  收藏  举报