数据结构专题-学习笔记:分块
1.概述
分块,被称为优雅的暴力,实质上分块就是一种暴力算法。但是分块因其优美性与可扩展性,使得很多题目往往用分块做更简洁。而分块的最重要的一句话就是:大块维护,小块朴素。
分块被称为暴力是因为其时间复杂度是 \(O(n\sqrt n)\) ,如果卡常不当就可能会被卡掉,或者直接卡成 \(O(n^2)\)。
莫队算法也是基于分块的(有兴趣了解莫队的可以看一看我的 这篇博文),并且很多用线段树、树套树等算法做的题目往往用分块可以吊打 std。因此,分块还是很重要的一种算法。
接下来通过一道题目,讲解分块的思想 & 实现。
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 反正我是不会。
这道题 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;
}
求区间众数。
这道题需要用到前缀和的思想。
令 \(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;
}
还是分块。
区间修改不说。关键是区间查询。
对于每一块我们维护一个 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.总结
分块的总结就一句话:大块维护,小块朴素!