整理:分块
关于分块的整理
1.何为分块
\(\textit {OI Wiki}\) 如是说:
分块的基本思想是,通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
说人话,就是将整体数据分为若干块,然后对每一块整体进行处理,就可以获得更优的时间复杂度
那更优的时间复杂度到底是什么样的时间复杂度呢?
\(\textit {OI Wiki}\) 如是说:
分块的时间复杂度主要取决于分块的块长,一般可以通过均值不等式求出某个问题下的最优块长,以及相应的时间复杂度。
说人话,最优情况一般是块长为 \(\sqrt n\) 的时候,时间复杂度为 \(O(n \sqrt n)\),但是当 \(\sqrt n\) 偏小的时候,我们可以手动设置一个较大的块长,来获得更优的时间复杂度
而分块在进行处理时,会出现不完整的块,对于不完整的块,我们暴力求解,对于完整的块,我们采用 \(O(1)\) 的方式求解
分块对区间类的问题有特攻效果
2.怎么进行分块
首先,我们要对总体数据进行分块,需要记录以下信息:
- 编号为 \(i\) 的点属于哪个块
- 每个块的左右端点
inline void build(){
B=sqrt(n)+1;
tomax(B,min(n,300));//防止块长过短
for(int i=1,now;i<=n;i++){
now=(i-1)/B+1;
id[i]=now;
if(!L[now])L[now]=i;
R[now]=i;
}
}
然后我们可以进行很多区间操作
1.修改
1.区间加
我们开一个 \(add\) 数组记录每一个块整体被加了多少,对于两边不完整的块,我们直接修改被修改的值
inline void Add(int l,int r,int c){
if(id[l]==id[r]){
for(int i=l;i<=r;i++)a[i]+=c;
return ;
}
for(int i=R[id[l]];i>=l;i--)a[i]+=c;
for(int i=L[id[r]];i<=r;i++)a[i]+=c;
for(int i=id[l]+1;i<id[r];i++)add[i]+=c;
return ;
}
2.区间乘
与区间加略有不同,原因在于在加上一个数之后乘和加上一个数之前乘我们得到的效果是不一样的
所以我们需要开两个数组 \(add\) 和 \(mul\),其中 \(mul\) 记录在加上数之前我们一共乘了多少
对于乘法操作中两边零散的块,我们直接将原来的操作落实下去,然后将 \(add\) 和 \(mul\) 分别设为 \(0\) 和 \(1\),然后直接修改被修改的值
对于乘法操作中完整的块,我们同时修改修改 \(mul\) 和 \(add\) 中的值即可
inline void change(int idx){//将第 idx 块的修改落实
for(int i=L[idx];i<=R[idx];i++)a[i]=(a[i]*mul[idx]%mod+add[idx])%mod;
add[idx]=0;
mul[idx]=1;
}
inline void Add(int l,int r,int c){
if(id[l]==id[r]){
change(id[l]);
for(int i=l;i<=r;i++)a[i]=(a[i]+c)%mod;
return ;
}
int idL=id[l],idR=id[r];
change(idL);
for(int i=R[idL];i>=l;i--)a[i]=(a[i]+c)%mod;
change(idR);
for(int i=L[idR];i<=r;i++)a[i]=(a[i]+c)%mod;
for(int i=idL+1;i<idR;i++)add[i]=(add[i]+c)%mod;
return ;
}
3.区间开方
我们要知道,开方的次数不会太多,最多 \(6\) 次,对于少于 \(6\) 次的区间,我们暴力修改,对于多于 \(6\) 次的区间,我们不进行修改,这样每个数最多被修改 \(6\) 次,时间复杂度 \(O(n)\)
inline void change(int l,int r){
if(id[l]==id[r]){
int now=id[l];
if(sum[now]==B)return ;//sum 表示此块之和,如果与块长相等,说明每个数都是 1,开方不会有任何改变
for(int i=l;i<=r;i++){
sum[now]-=a[i]-(int)sqrt(a[i]);
a[i]=sqrt(a[i]);
}
return ;
}
int idL=id[l],idR=id[r];
for(int i=R[idL];i>=l;i--){
sum[idL]-=a[i]-(int)sqrt(a[i]);
a[i]=sqrt(a[i]);
}
for(int i=L[idR];i<=r;i++){
sum[idR]-=a[i]-(int)sqrt(a[i]);
a[i]=sqrt(a[i]);
}
for(int i=id[l]+1;i<id[r];i++){
if(sum[i]==B)continue;
for(int j=L[i];j<=R[i];j++){
sum[i]-=a[j]-(int)sqrt(a[j]);
a[j]=sqrt(a[j]);
}
}
return ;
}
4.区间修改
将 \([l,r]\) 的区间全部变成同一个值
可以想到,进行多次操作之后,序列的值肯定大部分相同,既然这样,我们开 \(flag\) 数组记录某一块的值是否相等,\(val\) 记录如果值相等,那么这个值是什么
inline void change(int l,int r,int c){
int idL=id[l],idR=id[r];
if(idL==idR){
if(flag[idL])for(int i=L[idL];i<=R[idL];i++)a[i]=val[idL];
flag[idL]=0;
for(int i=l;i<=r;i++)a[i]=c;
return ;
}
int res=0;
if(flag[idL])for(int i=L[idL];i<=R[idL];i++)a[i]=val[idL];
flag[idL]=0;
for(int i=R[idL];i>=l;i--)a[i]=c;
if(flag[idR])for(int i=L[idR];i<=R[idR];i++)a[i]=val[idR];
flag[idR]=0;
for(int i=L[idR];i<=r;i++)a[i]=c;
for(int i=idL+1;i<idR;i++){
flag[i]=1;
val[i]=c;
}
}
5.单点插入
在某个位置插入一个数
我们使用 \(vector\) 存每一个块中的值,对于插入,我们找到这个位置属于哪一块,然后之间 \(insert\) 插入即可
尽管 \(insert\) 为线性时间复杂度,但是每一块的规模为 \(\sqrt n\),所以实际上的复杂度还是 \(\sqrt n\)
但是,如果向同一个位置多次插入,有一个块会变得巨大,导致单次插入时间复杂度达到 \(O(n)\),所以,我们在进行若干次操作后,重新分块即可(术语叫定期重构)
inline void build(bool opt){
if(opt){//定期重构
int tot=(n-1)/B+1;
n=0;
for(int i=1;i<=tot;i++){
for(int j:b[i])a[++n]=j;
b[i].clear();
}
}
B=sqrt(n)+1;
tomax(B,min(n,300ll));
for(int i=1,now;i<=n;i++)
b[(i-1)/B+1].push_back(a[i]);
}
inline void insert(int place,int x){
int idx=1,now=0;
while(idx<=n&&now+b[idx].size()<place){
now+=b[idx].size();
idx++;
}
b[idx].insert(b[idx].begin()+place-now-1,x);
}
2.查询
1.查询区间和
我们开一个数组 \(sum\),在分块和修改的时候顺便记录修改对 \(sum\) 的影响即可
查询时,我们对零散的块直接暴力统计,对整块,我们选择将 \(sum\) 相加
inline int query(int l,int r,int c){
int res=0;
if(id[l]==id[r]){
int now=id[l];
for(int i=l;i<=r;i++)res+=a[i]+add[now],res%=c+1;
return res;
}
int idL=id[l],idR=id[r];
for(int i=R[idL];i>=l;i--)res+=a[i]+add[idL],res%=c+1;
for(int i=L[idR];i<=r;i++)res+=a[i]+add[idR],res%=c+1;
for(int i=idL+1;i<idR;i++){
res+=sum[i];
res%=c+1;
}
return res;
}
2.区间内小于某个值的元素个数/询问区间内小于某个值的前驱(比其小的最大元素)
我们另外开一个 \(b\) 数组,记录每一块块内排序后的结果
修改时,对于零散的块,直接修改后重新排序,记录在 \(b\) 中
然后二分查找即可
inline int query(int l,int r,int c){//区间内小于某个值的元素个数
int res=0;
if(id[l]==id[r]){
int now=id[l];
for(int i=l;i<=r;i++){
if(a[i]<c-add[now])//注意统计 a 还是 b,考虑修改带来的影响,这里以区间加为例
res++;
}
return res;
}
int idL=id[l],idR=id[r];
for(int i=l;i<=R[idL];i++)
if(a[i]<c-add[idL])res++;
for(int i=L[idR];i<=r;i++)
if(a[i]<c-add[idR])res++;
for(int i=idL+1;i<idR;i++)
res+=lower_bound(b+L[i],b+R[i]+1,c-add[i])-b-L[i];
return res;
}
inline int query(int l,int r,int c){//询问区间内小于某个值的前驱(比其小的最大元素)
int res=-inf;
if(id[l]==id[r]){
int now=id[l];
for(int i=l;i<=r;i++)
if(a[i]<c-add[now])
tomax(res,a[i]+add[now]);
return res==-inf?-1:res;
}
int idL=id[l],idR=id[r];
for(int i=l;i<=R[idL];i++)
if(a[i]<c-add[idL])
tomax(res,a[i]+add[idL]);
for(int i=L[idR];i<=r;i++)
if(a[i]<c-add[idR])
tomax(res,a[i]+add[idR]);
for(int i=idL+1,p;i<idR;i++){
p=lower_bound(b+L[i],b+R[i]+1,c-add[i])-b-L[i];
if(p==0)continue;
tomax(res,b[p-1+L[i]]+add[i]);
}
return res==-inf?-1:res;
}
3.最小众数(不修改)
我们直接预处理出第 \(i\) 个块到第 \(j\) 个块的答案即可
inline void build(){
B=sqrt(n)+1;
tomax(B,min(n,300));
for(int i=1,now;i<=n;i++){
now=(i-1)/B+1;
id[i]=now;
if(!L[now])L[now]=i;
R[now]=i;
place[a[i]].push_back(i);
}
tot=(n-1)/B+1;
int cnt[N];
for(int i=1,mx,num;i<=tot;i++){
mx=num=0;
memset(cnt,0,sizeof(cnt));
for(int j=i;j<=tot;j++){
for(int k=L[j];k<=R[j];k++){
cnt[a[k]]++;
if(cnt[a[k]]>mx){
mx=cnt[a[k]];
num=a[k];
}else if(cnt[a[k]]==mx)tomin(num,a[k]);
}
mode[i][j]=num;
}
}
}
inline int find(int l,int r,int c){
int placer=upper_bound(place[c].begin(),place[c].end(),r)-place[c].begin();
int placel=lower_bound(place[c].begin(),place[c].end(),l)-place[c].begin();
return placer-placel;
}
inline int query(int l,int r){
int idL=id[l],idR=id[r],mx,num;
memset(vis,0,sizeof(vis));
if(idL==idR||idL+1==idR){
mx=num=0;
for(int i=l,tmp;i<=r;i++){
if(vis[a[i]])continue;
vis[a[i]]=1;
tmp=find(l,r,a[i]);
if(tomax(mx,tmp))num=a[i];
else if(mx==tmp)tomin(num,a[i]);
}
return tmp[num];
}
num=mode[idL+1][idR-1];
mx=find(l,r,num);
vis[num]=1;
for(int i=R[idL],tmp;i>=l;i--){
if(vis[a[i]])continue;
vis[a[i]]=1;
tmp=find(l,r,a[i]);
if(tomax(mx,tmp))num=a[i];
else if(mx==tmp)tomin(num,a[i]);
}
for(int i=L[idR],tmp;i<=r;i++){
if(vis[a[i]])continue;
vis[a[i]]=1;
tmp=find(l,r,a[i]);
if(tomax(mx,tmp))num=a[i];
else if(mx==tmp)tomin(num,a[i]);
}
return tmp[num];
}

浙公网安备 33010602011771号