分块
前言
在了解过树状数组和线段数之后,我们已经能处理许多区间的信息修改和查询的题目。但当信息不具有区间可加性时,用树状数组和线段树就不好处理了,这时候就可以用到一种优雅的暴力——分块。
简介
分块是一种思想,通过适当的划分,预处理一部分信息并保留,用空间换时间达到时空平衡,来完成对数列一些区间操作和区间查询的操作。效率比不上线段树和树状数组,大概是根号级别的,但它更加通用,容易实现,可以维护更复杂的信息。
具体操作
建块
首先要明确我们需要什么:
- 块的大小
- 块的数量
- 块的左右边界
- 每个元素所属的块
首先是块的大小,块的大小具体要依据实际,使时间复杂度最小。一般情况下大小为 \(\sqrt{n}\)。
sq=sqrt(n);
接下来是块的数量,要注意 \(n\) 不一定是个完全平方数,最后剩下的一些元素组成一个组。
tot=n/sq;
if(n%sq) tot++;
确定块的左右边界,直接找规律。\(L_1=1,R_1=sq,L_2=sq+1,R_2=2 \times sq , \cdots\)
得到规律:
\[L_i=(x-1) \times sq+1,R_x=x \times sq \]还要注意 \(R_{tot}=n\)
for(int i=1;i<=tot;i++)
{
L[i]=(i-1)*block+1;
R[i]=i*block;
}
R[tot]=n;
确定每个元素所属的块,还是找规律。
for(int i=1;i<=n;i++) b[i]=(i-1)/sq+1;
总体代码:
sq=sqrt(n);
tot=n/sq;
if(n%sq) tot++;
for(int i=1;i<=n;i++)
{
cin>>a[i];
b[i]=(i-1)/sq+1;
}
for(int i=1;i<=tot;i++)
{
L[i]=(i-1)*sq+1;
R[i]=i*sq;
}
R[tot]=n;
修改
以维护区间和为例。
修改区间为 \([l,r]\),这个区间覆盖的不一定都是整块,会出现只有块中几个元素的碎块,如图:
共有左碎块,整块,右碎块三个部分,分别处理。
设 \(x\),\(y\) 为左碎块和右碎块的块编号。
在整块中,不必具体维护每个元素,使用和线段树类似的懒标记,将修改的信息存在里面,又类似于标记永久化,不必去修改原数组中的元素,在使用的时候加上就可以了。也就是说\(a_i\) 实际上是 \(a_i+lazy_i\)。由图可知,整块的块编号为 \([x+1,y-1]\)。
对于碎块,碎块的元素个数最多不超过两倍的块的大小,可以直接暴力修改。由图可知,左碎块的区间为 \([l,R_x]\),右碎块的区间为 \([L_y,r]\)。
需要特判一下 \(x=y\) 的情况。
void update(int l,int r,int k)
{
int x=b[l],y=b[r];
if(x==y)
{
for(int i=l;i<=r;i++) a[i]+=k,v[x]+=k;//v为区间和
}
else
{
for(int i=l;i<=R[x];i++) a[i]+=k,v[x]+=k;//左碎块
for(int i=L[y];i<=r;i++) a[i]+=k,v[y]+=k;//右碎块
for(int i=x+1;i<=y-1;i++) lazy[i]+=k,v[i]+=k*sq;//整块
}
}
查询
同样以维护区间和为例。
与修改类似,对于整块,直接加和。对于碎块,暴力加答案。
int query(int l,int r,int k)
{
int ans=0;
int x=b[l],y=b[r];
if(x==y)
{
for(int i=l;i<=r;i++) ans+=a[i]+lazy[x];
}
else
{
for(int i=l;i<=R[x];i++) ans+=a[i]+lazy[x];
for(int i=L[y];i<=r;i++) ans+=a[i]+lazy[y];
for(int i=x+1;i<=y-1;i++) ans+=v[i];
}
return ans;
}
接下来就可以来做一些简单的例题了。
P2357 守墓人
https://www.luogu.com.cn/problem/P2357
分析
简单的板子题,将上述的一些操作结合就好,维护区间和。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5;
ll n,m,sq,tot,a[N],b[N],v[N],L[N],R[N],lazy[N];
void add(int l,int r,int k)
{
int x=b[l],y=b[r];
if(x==y)
{
for(int i=l;i<=r;i++) a[i]+=k,v[x]+=k;//v为区间和
}
else
{
for(int i=l;i<=R[x];i++) a[i]+=k,v[x]+=k;//左碎块
for(int i=L[y];i<=r;i++) a[i]+=k,v[y]+=k;//右碎块
for(int i=x+1;i<=y-1;i++) lazy[i]+=k,v[i]+=k*sq;//整块
}
}
ll query(int l,int r)
{
ll ans=0;
ll x=b[l],y=b[r];
if(x==y)
{
for(int i=l;i<=r;i++) ans+=a[i]+lazy[x];
}
else
{
for(int i=l;i<=R[x];i++) ans+=a[i]+lazy[x];
for(int i=L[y];i<=r;i++) ans+=a[i]+lazy[y];
for(int i=x+1;i<=y-1;i++) ans+=v[i];
}
return ans;
}
int main ()
{
ios::sync_with_stdio(false);
cin.tie(0);cout.tie(0);
cin>>n>>m;
sq=sqrt(n);
tot=n/sq;
if(n%sq) tot++;
for(int i=1;i<=n;i++)
{
cin>>a[i];
b[i]=(i-1)/sq+1;
v[b[i]]+=a[i];
}
for(int i=1;i<=tot;i++)
{
L[i]=(i-1)*sq+1;
R[i]=i*sq;
}
R[tot]=n;
ll op,l,r,k;
while(m--)
{
cin>>op;
if(op==1)
{
cin>>l>>r>>k;
add(l,r,k);
}
else if(op==2)
{
cin>>k;
add(1,1,k);
}
else if(op==3)
{
cin>>k;
add(1,1,-k);
}
else if(op==4)
{
cin>>l>>r;
cout<<query(l,r)<<"\n";
}
else cout<<query(1,1)<<"\n";
}
return 0;
}
P3870 [TJOI2009] 开关
https://www.luogu.com.cn/problem/P3870
分析
也是类似于板子题,不过操作变成了区间取反。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;
int a[N],b[N],lazy[N],v[N],m,n,sq;
void change(int i)
{
v[b[i]]-=a[i]^lazy[b[i]],a[i]^=1,v[b[i]]+=a[i]^lazy[b[i]];
}
void update(int l,int r)
{
int x=b[l],y=b[r];
if(x==y) for(int i=l;i<=r;i++) change(i);
else
{
for(int i=l;i<=x*sq;i++) change(i);
for(int i=(y-1)*sq+1;i<=r;i++) change(i);
for(int i=x+1;i<=y-1;i++) lazy[i]^=1,v[i]=sq-v[i];
}
}
int query(int l,int r)
{
int x=b[l],y=b[r],ans=0;
if(x==y) for(int i=l;i<=r;i++) ans+=a[i]^lazy[b[i]];
else
{
for(int i=l;i<=x*sq;i++) ans+=a[i]^lazy[b[i]];
for(int i=(y-1)*sq+1;i<=r;i++) ans+=a[i]^lazy[b[i]];
for(int i=x+1;i<=y-1;i++) ans+=v[i];
}
return ans;
}
int main ()
{
cin>>n>>m;
sq=sqrt(n);
for(int i=1;i<=n;i++) b[i]=(i-1)/sq+1;
while(m--)
{
int op,x,y;
cin>>op>>x>>y;
if(op==0) update(x,y);
else cout<<query(x,y)<<"\n";
}
return 0;
}
P2801 教主的魔法
https://www.luogu.com.cn/problem/P2801
分析
求区间中大于等于 \(c\) 的个数。可以将每个块分别排序。
对于整块来说,因为已经排好了序,所以可以二分查找。对于碎块来说,也是暴力统计。注意修改时要重新排序碎块,整块不用排序。
时间复杂度为 \(O(n \log{n})\)。
code
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+5;
int n,m,tot,sq,R[N],L[N],lazy[N],a[N],b[N],c[N];
void update(int l,int r,int k)
{
int x=b[l],y=b[r];
if(x==y)
{
for(int i=l;i<=r;i++) a[i]+=k;
for(int i=L[x];i<=R[y];i++) c[i]=a[i];
sort(c+L[x],c+R[y]+1);
}
else
{
for(int i=l;i<=R[x];i++) a[i]+=k;
for(int i=L[x];i<=R[x];i++) c[i]=a[i];
sort(c+L[x],c+R[x]+1);
for(int i=L[y];i<=r;i++) a[i]+=k;
for(int i=L[y];i<=R[y];i++) c[i]=a[i];
sort(c+L[y],c+R[y]+1);
for(int i=x+1;i<=y-1;i++) lazy[i]+=k;
}
}
int query(int l,int r,int k)
{
int ans=0,x=b[l],y=b[r];
if(x==y)
{
//这里不能写成一行QAQ,不然else会与后面的if相关
for(int i=l;i<=r;i++)
if(lazy[x]+a[i]>=k)
ans++;
}
else
{
for(int i=l;i<=R[x];i++) if(lazy[x]+a[i]>=k) ans++;
for(int i=L[y];i<=r;i++) if(lazy[y]+a[i]>=k) ans++;
for(int i=x+1;i<=y-1;i++)
{
int ll=L[i],rr=R[i],cnt=0,mid;
while(ll<=rr)
{
mid=(ll+rr)>>1;
if(c[mid]+lazy[i]>=k) rr=mid-1,cnt=R[i]-mid+1;
else ll=mid+1;
}
ans+=cnt;
}
}
return ans;
}
int main ()
{
ios::sync_with_stdio(0);
cin.tie(0);cout.tie(0);
cin>>n>>m;
sq=sqrt(n);
tot=n/sq;
if(n%sq) tot++;
for(int i=1;i<=n;i++)
{
cin>>a[i];
b[i]=(i-1)/sq+1;
c[i]=a[i];
}
for(int i=1;i<=tot;i++)
{
L[i]=(i-1)*sq+1;
R[i]=i*sq;
}
R[tot]=n;
for(int i=1;i<=tot;i++) sort(c+L[i],c+R[i]+1);
char op;
int l,r,k;
while(m--)
{
cin>>op>>l>>r>>k;
if(op=='M') update(l,r,k);
else cout<<query(l,r,k)<<"\n";
}
return 0;
}
标签:分块,int,sq,tot,区间,op,碎块
From: https://www.cnblogs.com/zhouruoheng/p/18025432