基础分块

分块

分块是一种优雅的暴力,虽然一般只是\(O(n\sqrt{n})\)的复杂度,但由于常数较小,一些时候可以取得比一些\(O(n\log{n})\)的数据结构更优的成绩
分块的基本思想就是通过对原数据的适当划分,将大问题划分为多个小块上处理的问题,以此得到比其他暴力更优的复杂度
分块思想应用广泛,各种难度不同功效不同的分块都会在题目中出现。本文主要介绍块状数组这一分支

块状数组

块状数组的核心思想是把数组分为一定长度的小块,进行操作时,遇到完整的块就整体标记,对于不完整的散块则暴力处理,以此减小暴力复杂度
常见的操作包括:区间加、减;询问区间大于或小于某个数的数的个数;询问区间第k大数等
为了建立一个块状数组,需要维护以下信息:
\(len\) :块长,通常设为\(\sqrt{n}\),也可以根据题目要求调整
\(tot\) :块的数量,需要考虑到不整除时最后有一块长度较短的块
\(belong\) :数组,\(belong_{i}\)表示\(i\)号元素从属的块的编号
\(st\) :数组,存储每个块的左端点
\(ed\) :数组,存储每个块的右端点
\(t\) :数组,原数组的副本,用于排序后存储每一块内的大小关系
\(n\) :数的总量
\(a\) :数组,原来的数列

对于一个块状数组,应有以下操作:

建块

确定块的大小、数量,记录每个块的左右端点,同时对每个块内的数据进行初始化
块的大小一般设为\(\sqrt{n}\),理论依据是通过基本不等式推算。由于各个题目的数据范围和题目要求可能不同,所以可疑因题而异改变块长。一般通过以下代码实现:

len=(int)(sqrt(n));

块的个数显然就是总数除以块长,除不尽就多加一块:

tot=n/len;
if(n%len) tot++;

对于块的左右边界,容易得到\(st_{1}=1,ed_{1}=len,st_{2}=len+1,ed_{2}=2*len...\)
所以可以得到通项式\(st_{i}=len*(i-1)+1, ed_{i}=i*len\),代码如下:

for(int i=1;i<=tot;i++){
    st[i]=(i-1)*len+1;
    ed[i]=min(i*len,n);
}

分配块时,可以容易地得到公式\(belong_{i}=\frac{i-1}{len}+1\)

for(int i=1;i<=n;i++){
    belong[i]=(i-1)/len+1;
}

对于块内的初始化,因题而异。例如教主的魔法中初始化就是排序

区间操作

这里以区间加为例
考虑线段树思想,利用lazytag储存整块的操作。对于零块或者只修改部分整块的情况,暴力修改,这样每个数的真实值其实是\(a_{i}+tag_{i}\)。注意如果涉及排序,需要把数组备份一份,也就是前文提到的\(t\)数组。同时,需要特判\(belong_{l}==belong_{r}\)也就是只有一个点的情况,代码如下:

//区间加(含排序)
void modify(int l,int r,int k){
    int fl=belong[l],fr=belong[r];
    //区间在整块内直接修改
    if(fl==fr){
        for(int i=l;i<=r;i++){a[i]+=k;}
        Sort(fl);
        return;
    }
    //暴力修改左右非整块的区间
    for(int i=l;i<=ed[fl];i++){a[i]+=k;}
    Sort(fl);
    for(int i=st[fr];i<=r;i++){a[i]+=k;}
    Sort(fr);
    //中间的整块打上标记
    for(int i=fl+1;i<fr;i++) tg[i]+=k;
    return;
}

区间减和区间加相同,只需要注意改换标记的种类
对于区间赋值,原理类似,但需要设置\(pushdown\)函数。对于\(tag\)数组,需要用一个特殊值标记没有获得修改的tag,对于零块需要在下一次操作之前pushdown下传标记,同时把标记信息下放,然后重新排序。代码:

void pushdown(int x) {
    if (tag[x]!=2147483647)  // 用该值标记块内没有被整体赋值
        for(int i=st[x];i<=ed[x];i++) a[i]=t[i]=tag[x];
    tag[x]=2147483647
}

void Modify(int l,int r,int k) {
    int fl=belong[l],fr=belong[r];
    pushdown(fl);
    if(fl==fr){
        for(int i=l;i<=r;i++) a[i]=k;
        Sort(fl);
        return;
    }
    pushdown(fr);
    for(int i=l;i<=ed[fl];i++)a[i]=k;
    for(int i=st[fr];i<=r;i++) a[i]=k;
    Sort(fl);Sort(fr);
    for(int i=x+1;i<y;i++)tag[i]=k;
}

区间查询

对于查询区间内元素和,暴力加和即可,注意还要加上lazytag
对于区间内比k大/小的元素个数,分类处理:对于整块,排序后进行二分查找;对于边角零块,暴力枚举,代码:

//区间大于等于k的个数
int kth(int l,int r,int k){
    int res=0;
    int fl=belong[l],fr=belong[r];
    //区间在整块内暴力查找
    if(fl==fr){
        for(int i=l;i<=r;i++){
            //先前对整块打上了tg标记,此时应该加上tg
            if(a[i]+tg[fl]>=k) res++;
        }
        return res;
    }
    //暴力计算两侧边角块
    for(int i=l;i<=ed[fl];i++){if(a[i]+tg[fl]>=k) res++;}
    for(int i=st[fr];i<=r;i++){if(a[i]+tg[fr]>=k) res++;}
    //二分查找整块部分
    /*用lower_bound求出大于等于k的第一个数,块长减去这个数的位置得到块内答案,所有块累加。*/
    for(int i=fl+1;i<=fr-1;i++){
        res+=ed[i]-(lower_bound(t+st[i],t+ed[i]+1,k-tg[i])-t)+1;
    }
    return res;
}

//区间内小于等于k的值个数
int qnum(int l,int r,int k){
    int res=0;
    int fl=belong[l],fr=belong[r];
    //区间在整块内暴力查找
    if(fl==fr){
        for(int i=l;i<=r;i++){
            //先前对整块打上了tg标记,此时应该加上tg
            if(a[i]+tg[fl]<=k) res++;
        }
        return res;
    }
    //暴力计算两侧边角块
    for(int i=l;i<=ed[fl];i++){
        if(a[i]+tg[fl]<=k) res++;
    }
    for(int i=st[fr];i<=r;i++){
        if(a[i]+tg[fr]<=k) res++;
    }
    //二分查找整块部分
    for(int i=fl+1;i<fr;i++){
        //整个块都比k大,跳过
        if(t[st[i]]+tg[i]>k) continue;
        if(t[ed[i]]+tg[i]<=k){
            //整个块都比k小,直接累加
            res+=ed[i]-st[i]+1;
            continue;
        }
        //二分
        int ll=st[i],rr=ed[i];
        while(ll<rr){
            int mid=((ll+rr)>>1)+1;
            if(t[mid]+tg[i]<=k) ll=mid;
            else rr=mid-1;
        }
        if(t[ll]+tg[i]<=k) res+=ll-st[i]+1;
    }
    return res;
}

对于区间第k大数,可以考虑二分答案,在此基础上利用求区间小于k的数的个数的技巧进行\(check\),详见第二道例题

例题

洛谷P2801教主的魔法
裸的分块题,准许区间修改并维护区间内大于k的数的个数,亮点在于对于每一个块进行了排序,方便利用stl进行二分查找
代码

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 1000010

char op;
int n,m,x,y,k;
int a[N];
int len;//块长
int tot;//块数
int st[N];//所有块的起点
int ed[N];//所有块的终点
int tg[N];//区间操作标记
int t[N];//从属块
int belong[N];//某个元素所属的块

//排序
void Sort(int k){
    for(int i=st[k];i<=ed[k];i++) t[i]=a[i];//更新t数组
    sort(t+st[k],t+ed[k]+1);//排序
}

//初始化
void init(){
    //计算块长和块的总数
    len=(int)sqrt((double)(n));
    tot=n/len;
    if(n%len) tot++;
    //为每个元素分配块
    for(int i=1;i<=n;i++){
        belong[i]=(i-1)/len+1;
    }
    //标记每个块的左右边界
    for(int i=1;i<=tot;i++){
        st[i]=(i-1)*len+1;
        ed[i]=min(i*len,n);
        Sort(i);
    }
	
}

//区间加
void modify(int l,int r,int k){
    int fl=belong[l],fr=belong[r];
    //区间在整块内直接修改
    if(fl==fr){
        for(int i=l;i<=r;i++){a[i]+=k;}
        Sort(fl);
        return;
    }
    //暴力修改左右非整块的区间
    for(int i=l;i<=ed[fl];i++){a[i]+=k;}
    for(int i=st[fr];i<=r;i++){a[i]+=k;}
    //中间的整块打上标记
    for(int i=fl+1;i<fr;i++) tg[i]+=k;
    Sort(fl);Sort(fr);
    return;
}

//区间大于等于k的个数
int kth(int l,int r,int k){
    int res=0;
    int fl=belong[l],fr=belong[r];
    //区间在整块内暴力查找
    if(fl==fr){
        for(int i=l;i<=r;i++){
            //先前对整块打上了tg标记,此时应该加上tg
            if(a[i]+tg[fl]>=k) res++;
        }
        return res;
    }
    //暴力计算两侧边角块
    for(int i=l;i<=ed[fl];i++){if(a[i]+tg[fl]>=k) res++;}
    for(int i=st[fr];i<=r;i++){if(a[i]+tg[fr]>=k) res++;}
    //二分查找整块部分
    /*用lower_bound求出大于等于k的第一个数,块长减去这个数的位置得到块内
    答案,所有块累加。*/
    for(int i=fl+1;i<=fr-1;i++){
        res+=ed[i]-(lower_bound(t+st[i],t+ed[i]+1,k-tg[i])-t)+1;
    }
    return res;
}

signed main(){
    ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
    cin>>n>>m;
    for(int i=1;i<=n;i++){
        cin>>a[i];
        t[i]=a[i];//创建一份副本用来排序,映射原数组的大小关系
    }
    init();
    for(int i=1;i<=m;i++){
        cin>>op>>x>>y>>k;
        if(op=='M') modify(x,y,k);
        if(op=='A') cout<<kth(x,y,k)<<endl;
    }
    return 0;
}

洛谷P5386由乃打扑克
Ynoi难得的不太卡常不太毒瘤的题,维护两个操作,区间加按板子直接上就行;区间第k小值考虑在值域上二分答案,用查询区间内小于k的数的个数进行check。考虑到每个数的大小为\(-2\times{10}^{4}\leq2\times{10}^{4}\)同时区间操作次数达到了\(1\times{10}^{5}\),值域大小会达到\(2\times{10}^{9}\)级别,所以考虑维护区间最大、最小值用来规定值域,削减二分次数。总复杂度\(O(n\sqrt{n}\log^{2}{V})\)可过。代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
#define N 100010
#define inf 2e9+7
#define int long long

int n,m,op,x,y,k;
int a[N];
int len;//块长
int tot;//块数
int st[N];//所有块的起点
int ed[N];//所有块的终点
int tg[N];//区间操作标记
int t[N];//从属块
int belong[N];//某个元素所属的块

//排序
void Sort(int k){
    for(int i=st[k];i<=ed[k];i++) t[i]=a[i];//更新t数组
    sort(t+st[k],t+ed[k]+1);//排序
}

//初始化
void init(){
    //计算块长和块的总数
    len=(int)(sqrt(n));
    tot=n/len;
    if(n%len) tot++;
    //为每个元素分配块
    for(int i=1;i<=n;i++){
        belong[i]=(i-1)/len+1;
    }
    //标记每个块的左右边界
    for(int i=1;i<=tot;i++){
        st[i]=(i-1)*len+1;
        ed[i]=min(i*len,n);
        sort(t+st[i],t+ed[i]+1);
    }
    return;
}

//区间加
void modify(int l,int r,int k){
    int fl=belong[l],fr=belong[r];
    //区间在整块内直接修改
    if(fl==fr){
        for(int i=l;i<=r;i++){a[i]+=k;}
        Sort(fl);
        return;
    }
    //暴力修改左右非整块的区间
    for(int i=l;i<=ed[fl];i++){a[i]+=k;}
    Sort(fl);
    for(int i=st[fr];i<=r;i++){a[i]+=k;}
    Sort(fr);
    //中间的整块打上标记
    for(int i=fl+1;i<fr;i++) tg[i]+=k;
    return;
}

//区间内小于等于k的值个数
int qnum(int l,int r,int k){
    int res=0;
    int fl=belong[l],fr=belong[r];
    //区间在整块内暴力查找
    if(fl==fr){
        for(int i=l;i<=r;i++){
            //先前对整块打上了tg标记,此时应该加上tg
            if(a[i]+tg[fl]<=k) res++;
        }
        return res;
	}
	//暴力计算两侧边角块
	for(int i=l;i<=ed[fl];i++){
		if(a[i]+tg[fl]<=k) res++;
	}
	for(int i=st[fr];i<=r;i++){
		if(a[i]+tg[fr]<=k) res++;
	}
	//二分查找整块部分
	for(int i=fl+1;i<fr;i++){
		//整个块都比k大,跳过
		if(t[st[i]]+tg[i]>k) continue;
		if(t[ed[i]]+tg[i]<=k){
			//整个块都比k小,直接累加
			res+=ed[i]-st[i]+1;
			continue;
		}
		//二分
		int ll=st[i],rr=ed[i];
		while(ll<rr){
			int mid=((ll+rr)>>1)+1;
			if(t[mid]+tg[i]<=k) ll=mid;
			else rr=mid-1;
		}
		if(t[ll]+tg[i]<=k) res+=ll-st[i]+1;
	}
	return res;
}

//寻找二分上下界
int qmx(int l,int r){
	int res=-inf;
	int fl=belong[l],fr=belong[r];
	if(fl==fr){
		for(int i=l;i<=r;i++){
			res=max(res,a[i]+tg[fl]);
		}
		return res;
	}
	for(int i=l;i<=ed[fl];i++){res=max(res,a[i]+tg[fl]);}
	for(int i=st[fr];i<=r;i++){res=max(res,a[i]+tg[fr]);}
	for(int i=fl+1;i<fr;i++){
		res=max(res,t[ed[i]]+tg[i]);
	}
	return res;
}
int qmn(int l,int r){
	int res=inf;
	int fl=belong[l],fr=belong[r];
	if(fl==fr){
		for(int i=l;i<=r;i++){
			res=min(res,a[i]+tg[fl]);
		}
		return res;
	}
	for(int i=l;i<=ed[fl];i++){res=min(res,a[i]+tg[fl]);}
	for(int i=st[fr];i<=r;i++){res=min(res,a[i]+tg[fr]);}
	for(int i=fl+1;i<fr;i++){
		res=min(res,t[st[i]]+tg[i]);
	}
	return res;
}

//二分答案
void Find(int l,int r,int k){
	if(k<1||k>r-l+1) cout<<-1<<'\n';
	int ll=qmn(l,r);
	int rr=qmx(l,r);
	while(ll<=rr){
		int mid=(ll+rr)>>1;
		if(qnum(l,r,mid)<k){
			ll=mid+1;
		}else{
			rr=mid-1;
		}
	}
	cout<<ll<<'\n';
	return;
}

signed main(){
	freopen("由乃打扑克.txt","r",stdin);
	freopen("YnPokeOut.txt","w",stdout);
	ios::sync_with_stdio(0),cin.tie(0),cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=n;i++){
		cin>>a[i];
		t[i]=a[i];
	}
	init();
	for(int i=1;i<=m;i++){
		cin>>op>>x>>y>>k;
		if(op==1) Find(x,y,k);
		if(op==2) modify(x,y,k);
	}
	
	return 0;
}

posted @ 2025-03-26 20:12  Yun_Mo_s5_013  阅读(26)  评论(0)    收藏  举报