数据结构专题-学习笔记:分块

1.概述

分块,被称为优雅的暴力,实质上分块就是一种暴力算法。但是分块因其优美性与可扩展性,使得很多题目往往用分块做更简洁。而分块的最重要的一句话就是:大块维护,小块朴素。

分块被称为暴力是因为其时间复杂度是 \(O(n\sqrt n)\) ,如果卡常不当就可能会被卡掉,或者直接卡成 \(O(n^2)\)

莫队算法也是基于分块的(有兴趣了解莫队的可以看一看我的 这篇博文),并且很多用线段树、树套树等算法做的题目往往用分块可以吊打 std。因此,分块还是很重要的一种算法。

接下来通过一道题目,讲解分块的思想 & 实现。

2.思想

事实上,分块的思想非常重要,因为根据这个思想,我们可以解决很多的题目。

分块没有固定的模板,所以这里丢一道树状数组的模板题吧。

P3368 【模板】树状数组 2

操作:区间加法,单点差值。

这道题有很多很多的做法,包括 暴力,树状数组,线段树等等。

那么用分块怎么做呢?

分块分块,肯定要将数组划分成若干个块。

设块长为 \(S\) ,我们将 \([1,S]\) 分为第一块, \([S+1,2S]\) 分为第二块,······。

需要注意的是最后一块的长度可能会小于 \(S\)

我们将第 \(i\) 个元素所在的块存在 \(ys_i\) 里面, \(ys_i\) 的计算公式如下:\(ys_i=(i-1)/s+1\),其中的除法操作为整除操作。

这样,我们就完成了 划分 这一步骤。

接下来根据 大块维护,小块朴素 的思想,我们需要开一个 \(tag_i\) 数组表示当前 \(i\) 这一块 整体需要加减多少

看操作一:区间加法 \([l,r]+k\)

还是根据 大块维护,小块朴素 的思想,如果 \(ys_l=ys_r\) ,说明在同一个块内,朴素暴力更新即可。

否则,我们需要进行 大块维护

附张图:

在这里插入图片描述

上图中我们可以看到, \(l,r\) 在 2,6 两块,而 3,4,5 三块是整块。

因此,2,6 是小块, 3,4,5 是大块。

那么,首先暴力更新 2,6 当中受到影响的块。直接 \(a_i+=k\) 即可。

这里介绍一下已知块长 \(S\) ,当前为第 \(i\) 块怎么计算这块的左右端点:

左端点:\((i-1)*s+1\)

右端点:\(\min{(n,i*s)}\)

不理解的可以拿出纸笔算一下。

暴力更新玩 2,6 两块之后,我们要维护 3,4,5 三块。

还记得 \(tag\) 数组吗?它就是这个时候用的。

我们让 \(tag_3+=k,tag_4+=k,tag_5+=k\) 即可。因为我们要大块维护,所以这里 \(tag\) 就起到了这个作用。单点查询时直接扯过来用就好。

看单点查询 \(l\)

很简单!我们刚才维护了 \(tag_{ys_l}\) ,那么答案直接就是 \(a_l+tag_{ys_l}\) 。请读者考虑这是为什么。考虑成功了,说明分块已经掌握。

最后的时间复杂度是 \(O(n*(\dfrac{n}{s}+s))\),由基本不等式可知最优块长为 \(\sqrt n\) ,所以为 \(O(n\sqrt n)\)

这里需要说明的是:在每道题中块长不一定都是 \(\sqrt n\) ,所以要根据实际情况分析。常用的块长有 \(\sqrt n\)\(n^{\frac{2}{3}}\)

代码如下:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=5e5+5;
int n,b,op,l,r,c;
typedef long long LL;
LL a[MAXN],sum[MAXN],tag[MAXN];

void add(int l,int r,int k)
{
	int idl=l/b,idr=r/b;
	if(idl==idr)
	{
		for(int i=l;i<=r;i++)
		{
			a[i]+=k;sum[idl]+=k;
		}
	}
	else
	{
		for(int i=l;i<(idl+1)*b;i++)
		{
			a[i]+=k;sum[idl]+=k;
		}
		for(int i=idl+1;i<=idr-1;i++)
		{
			tag[i]+=k;sum[i]+=k*b;
		}
		for(int i=idr*b;i<=r;i++)
		{
			a[i]+=k;sum[idr]+=k;
		}
	}
}

int read()
{
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+ch-'0';ch=getchar();}
	return sum*fh;
}

int main()
{
	n=read();b=sqrt(n);
	for(int i=1;i<=n;i++)
	{
		a[i]=read();sum[i/b]+=a[i];
	}
	for(int i=1;i<=n;i++)
	{
		op=read();l=read();r=read();c=read();
		if(op==0) add(l,r,c);
		else cout<<a[r]+tag[r/b]<<"\n";
	}
	return 0;
}

总结一下:分块的思路就是将数列划分成若干块,根据 大块维护,小块朴素 的思想来解决。根据题目需要,可能我们会开各种各样的数组维护块内元素。

上面那道题同时也是 Hzwer之数列分块入门 1。 Hzwer之数列分块入门 系列总共有 9 道题,推荐各位看一看这篇博客:「分块」数列分块入门1 – 9 by hzwer ,以更好的了解分块。

接下来是几道例题。

3.例题

题单:

没错,你没有看错,只要三道题!

分块的题目还是比较好想的 (暴力难道不好想吗qwq) ,所以只有三道题。

当然 Ynoi 除外。 如果认为上述题目太简单也可以把 lxl 的毒瘤 Ynoi 题切了。题单1 题单2 反正我是不会。

P3203 [HNOI2010]弹飞绵羊

这道题 LCT 可做,但是我们看看分块有什么表现。

我们维护两个值 \(Next_i,step_i\)\(Next_i\) 表示 \(i\) 跳出这个块 之后在哪个位置, \(step_i\) 为步数。

首先逆序维护一遍 \(Next_i,step_i\) 。取块长为 \(\sqrt n\)

查询操作?直接不断令 \(ans+=step_x,x=Next_x\)即可。跳出去就停。复杂度 \(\sqrt n\)

修改操作?暴力重构这个块即可。

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 2e5 + 10;
int n, m, a[MAXN], Next[MAXN], step[MAXN], block, ys[MAXN], bnum;

int read()
{
	int sum = 0; char ch = getchar();
	while(ch < '0' || ch > '9') ch = getchar();
	while(ch >= '0' && ch <= '9') {sum = (sum << 3) + (sum << 1) + (ch ^ 48); ch = getchar();}
	return sum;
}

int ask(int x)
{
	int sum = 0;
	while(x <= n)
	{
		sum += step[x];
		x = Next[x];
	}
	return sum;
}

void exchange(int op)
{
	int l = (ys[op] - 1) * block + 1, r = ys[op] * block;
	if(r > n) r = n;
	for(int i = r; i >= l; --i)
	{
		if(i + a[i] > r)
		{
			Next[i] = i + a[i];
			step[i] = 1;
		}
		else
		{
			Next[i] = Next[i + a[i]];
			step[i] = step[i + a[i]] + 1;
		}
	}
}

int main()
{
	n = read(); block = sqrt(n); bnum = ceil((double)n / block);
	for(int i = 1; i <= n; ++i) {ys[i] = (i - 1) / block + 1; a[i] = read();}
	for(int i = bnum; i >= 1; --i)
	{
		int l = (i - 1) * block + 1, r = min(n, i * block);
		for(int j = r; j >= l; --j)
		{
			if(j + a[j] > r)
			{
				Next[j] = j + a[j];
				step[j] = 1;
			}
			else
			{
				Next[j] = Next[j + a[j]];
				step[j] = step[j + a[j]] + 1;
			}
		}
	}
	m = read();
	for(int i = 1; i <= m; ++i)
	{
		int opt, j, k;
		opt = read(); j = read() + 1;
		if(opt == 1) printf("%d\n", ask(j));
		else
		{
			k = read();
			a[j] = k;
			exchange(j);
		}
	}
	return 0;
}

P4168 [Violet]蒲公英

求区间众数。

这道题需要用到前缀和的思想。

\(s_{i,j}\) 表示前 \(i\)\(j\) 的出现次数(离散化),\(ans_{i,j}\) 表示第 \(i\) 块到第 \(j\) 块的众数及其出现次数(用结构体)。

首先 \(O(n\sqrt n)\) 跑一遍。

然后如果 \([l,r]\) 的区间在一个块内,暴力!

否则我们先取出 \(ans_{ys_l+1,ys_r-1}\) 作为初始答案,在两边的小块暴力的时候判断一下这个数是否会称为众数,能就更新答案。

所以做完了?

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=4e4+10,BLOCK=300+10;
int n,m,a[MAXN],lastans,s[BLOCK][MAXN],b[MAXN],lastn,ys[MAXN],lsh[MAXN],block,bnum,cnt[MAXN];
struct node
{
	int cnt,num;
}ans[BLOCK][BLOCK];

int read()
{
	int sum=0,fh=1;char ch=getchar();
	while(ch<'0'||ch>'9') {if(ch=='-') fh=-1;ch=getchar();}
	while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+(ch^48);ch=getchar();}
	return sum*fh;
}

int ask(int l,int r)
{
	node tmp=ans[ys[l]+1][ys[r]-1];
	if(ys[l]==ys[r])
	{
		memset(cnt,0,sizeof(cnt));tmp=(node){0,0};
		for(int i=l;i<=r;i++)
		{
			cnt[lsh[i]]++;
			if(cnt[lsh[i]]>tmp.cnt) {tmp.cnt=cnt[lsh[i]];tmp.num=lsh[i];}
			else if(cnt[lsh[i]]==tmp.cnt) {tmp.cnt=cnt[lsh[i]];tmp.num=min(tmp.num,lsh[i]);}
		}
	}
	else
	{
		memset(cnt,0,sizeof(cnt));
		int ll=(ys[l]-1)*block+1,lr=ys[l]*block,rl=(ys[r]-1)*block+1,rr=min(n,ys[r]*block);
		for(int i=l;i<=lr;i++)
		{
			cnt[lsh[i]]++;int t=s[ys[r]-1][lsh[i]]-s[ys[l]][lsh[i]];
			if(cnt[lsh[i]]+t>tmp.cnt) {tmp.cnt=cnt[lsh[i]]+t;tmp.num=lsh[i];}
			else if(cnt[lsh[i]]+t==tmp.cnt) {tmp.cnt=cnt[lsh[i]]+t;tmp.num=min(tmp.num,lsh[i]);}
		}
		for(int i=rl;i<=r;i++)
		{
			cnt[lsh[i]]++;int t=s[ys[r]-1][lsh[i]]-s[ys[l]][lsh[i]];
			if(cnt[lsh[i]]+t>tmp.cnt) {tmp.cnt=cnt[lsh[i]]+t;tmp.num=lsh[i];}
			else if(cnt[lsh[i]]+t==tmp.cnt) {tmp.cnt=cnt[lsh[i]]+t;tmp.num=min(tmp.num,lsh[i]);}
		}
	}
//	for(int i=1; i<=n; ++i) cout << cnt[lsh[i]] << "\n";
	return b[tmp.num];
}

int main()
{
	n=read();m=read();block=sqrt(n);bnum=ceil((double)n/block);
	for(int i=1;i<=n;i++) {a[i]=read();ys[i]=(i-1)/block+1;b[i]=a[i];}
	sort(b+1,b+n+1);lastn=unique(b+1,b+n+1)-(b+1);
	for(int i=1;i<=n;i++) lsh[i]=lower_bound(b+1,b+lastn+1,a[i])-b;
	for(int i=1;i<=bnum;i++)
	{
		for(int j=1;j<=lastn;j++) s[i][j]=s[i-1][j];
		int l=(i-1)*block+1,r=i*block;
		if(r>n) r=n;
		for(int j=l;j<=r;j++) s[i][lsh[j]]++;
	}
//	for(int i=1;i<=lastn;i++) cout << b[i] << " ";
//	cout << "\n";
//	for(int i=1;i<=n;i++) cout << lsh[i] << " ";
	for(int i=1;i<=bnum;i++)
	{
		node tmp=(node){0,0};
		memset(cnt,0,sizeof(cnt));
		for(int j=i;j<=bnum;j++)
		{
			for(int k=(j-1)*block+1;k<=min(n,j*block);k++)
			{
				cnt[lsh[k]]++;
				if(cnt[lsh[k]]>tmp.cnt) {tmp.cnt=cnt[lsh[k]];tmp.num=lsh[k];}
				else if(cnt[lsh[k]]==tmp.cnt) {tmp.cnt=cnt[lsh[k]];tmp.num=min(tmp.num,lsh[k]);}
			}
			ans[i][j]=tmp;
		}
	}
//	cout << block << "\n" << bnum << "\n";
//	for(int i=1;i<=bnum;i++)
//		for(int j=1;j<=bnum;j++) cout << i << " " << j << " " << ans[i][j].cnt << " " << ans[i][j].num << "\n";
	for(int i=1;i<=m;i++)
	{
		int l=read(),r=read();
		l=(l+lastans-1)%n+1;r=(r+lastans-1)%n+1;
		if(l>r) swap(l,r);
		printf("%d\n",lastans=ask(l,r));
	}
	return 0;
}

P2801 教主的魔法

还是分块。

区间修改不说。关键是区间查询。

对于每一块我们维护一个 vector 表示这一块内的元素排序后的结果。小块修改时暴力重构。查询时,小块直接暴力,大块用二分查询答案即可。

代码:

#include<bits/stdc++.h>
using namespace std;

const int MAXN=1e6+5;
int n,q,ys[MAXN],block;
typedef long long LL;//不开 long long 见祖宗
LL a[MAXN],tag[MAXN];
vector<LL>v[MAXN];

int read()
{
	int sum=0;char ch=getchar();
	while(ch<'0'||ch>'9') ch=getchar();
	while(ch>='0'&&ch<='9') {sum=(sum<<3)+(sum<<1)+(ch^48);ch=getchar();}
	return sum;
}

void add(int l,int r,LL k)
{
	if(ys[l]==ys[r])
	{
		int ll=(ys[l]-1)*block+1,lr=ys[l]*block;
		for(int i=l;i<=r;++i) a[i]+=k;
		v[ys[l]].clear();
		for(int i=ll;i<=lr;++i) v[ys[l]].push_back(a[i]);
		sort(v[ys[l]].begin(),v[ys[l]].end());
	}
	else
	{
		int ll=(ys[l]-1)*block+1,lr=ys[l]*block,rl=(ys[r]-1)*block+1,rr=ys[r]*block;
		for(int i=l;i<=lr;++i) a[i]+=k;
		v[ys[l]].clear();
		for(int i=ll;i<=lr;++i) v[ys[l]].push_back(a[i]);
		sort(v[ys[l]].begin(),v[ys[l]].end());
		for(int i=rl;i<=r;++i) a[i]+=k;
		v[ys[r]].clear();
		for(int i=rl;i<=rr;++i) v[ys[r]].push_back(a[i]);
		sort(v[ys[r]].begin(),v[ys[r]].end());
		for(int i=ys[l]+1;i<=ys[r]-1;++i) tag[i]+=k;
	}
}

int ask(int l,int r,LL k)
{
	int ll=(ys[l]-1)*block+1,lr=ys[l]*block,rl=(ys[r]-1)*block+1,rr=ys[r]*block,sum=0;
	if(ys[l]==ys[r])
	{
		for(int i=l;i<=r;++i)
			if(a[i]+tag[ys[l]]>=k) ++sum;
	}
	else
	{
		for(int i=l;i<=lr;++i)
			if(a[i]+tag[ys[l]]>=k) ++sum;
		for(int i=rl;i<=r;++i)
			if(a[i]+tag[ys[r]]>=k) ++sum;
		for(int i=ys[l]+1;i<=ys[r]-1;++i)
		{
			int p=lower_bound(v[i].begin(),v[i].end(),k-tag[i])-v[i].begin();
			sum+=v[i].size()-p;
		}
	}
	return sum;
}

void print(int x,char tail=0)
{
	if(x>9) print(x/10);
	putchar(x%10+48);
	if(tail) putchar(tail);
}

int main()
{
	n=read();q=read();block=900;
	for(int i=1;i<=n;++i) ys[i]=(i-1)/block+1;
	for(int i=1;i<=n;++i) a[i]=read();
	for(int i=1;i<=n;++i) v[ys[i]].push_back(a[i]);
	for(int i=1;i<=ys[n];++i) sort(v[i].begin(),v[i].end());
	for(int i=1;i<=q;++i)
	{
		char ch;int l,r,k;
		ch=getchar();
		while(ch==' '||ch=='\r'||ch=='\n') ch=getchar();
		l=read();r=read();k=read();
		if(ch=='M') add(l,r,k);
		else print(ask(l,r,k),'\n');
	}
	return 0;
}

4.总结

分块的总结就一句话:大块维护,小块朴素!

posted @ 2022-04-13 21:41  Plozia  阅读(183)  评论(0编辑  收藏  举报