分块算法及其应用

分块

望月悲叹的最初分块

分块,优雅的暴力
分块也是同线段树等结构一样,维护区间操作的,不同于线段树和树状数组的是,分块所维护的信息并不需要满足区间可加性,以此,分块可以处理许多线段树等结构不可以处理的问题
简单来说,分块就是将整个序列分为若干个大小相同的块(最后一个可能不同),然后对于每一个块再加以维护,在统计信息时,我们仅仅需要将所需区间 [ l , r ] [l,r] [l,r]中所完全包含的块的信息统计,然后对于两端零散的块执行暴力,修改同理

为了平衡复杂度,一般取块长 b l o c k = n block=\sqrt{n} block=n

//块的基本操作
block=sqrt(n),siz=n%block==0?n/block:n/block+1;
int get(int x){//x属于哪一个块
    return x%block?x/block+1:x/block;
}
int solve(int l,int r){  
//枚举l,r属于的区间
    if(get(r)-get(l)<2){  
        for(int i=l;i<=r;i++)……
    }
    else{  
        int lR=get(l)*block,rL=get(r)*(block-1)+1
        for(int i=l;i<=lR;i++)……
        for(int i=rL;i<=r;i++)……
        for(int i=get(l)+1;i<=get(r)-1;i++)……
    }
}

举一个简单的例子,分块维护区间加法,我们只需要将其分为块之后对于每一个块建立一个求和数组即可,然后类似线段树的,建立一个懒标记

int sum[1005],a[100005],lz[1005];
int block,siz,n,m;
int get(int x){
	return x%block:x/block+1:x/block;
}
void init(){
	scanf("%d",&n);
	block=sqrt(n),siz=n%block?n/block+1:block;
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
	}
	for(int i=1;i<=siz;i++){
		sum[get(i)]+=a[i];
	}
}
void change(int l,int r,int d){//将[l,r]+d 
	if(get(r)-get(l)<2){
		for(int i=l;i<=r;i++)a[i]+=d,sum[get(i)]+=d;
		return ;
	}
	int lR=get(l)*block,rL=(get(r)-1)*block+1;
	for(int i=l;i<=lR;i++)a[i]+=d,sum[get(i)]+=d;
	for(int i=rL;i<=r;i++)a[i]+=d,sum[get(i)]+=d;
	for(int i=get(l)+1;i<=get(r)-1;i++)lz[i]+=d;
} 
int ask(int l,int r){
	int ans=0;
	if(get(r)-get(l)<2){
		for(int i=l;i<=r;i++)ans+=a[i]+lz[get(i)];
	}
	int lR=get(l)*block,rL=(get(r)-1)*block+1;
	for(int i=l;i<=lR;i++)ans+=a[i]+lz[get(i)];
	for(int i=rL;i<=r;i++)ans+=a[i]+lz[get(i)];
	for(int i=get(l)+1;i<=get(r)-1;i++)ans+=sum[i]+lz[i]*block;
}

下面我们来探讨分块模型的各种扩展

激进突刺的第二分块

蒲公英:静态维护区间众数问题, n ≤ 4 × 1 0 4 , m ≤ 5 × 1 0 4 n\le 4\times 10^4,m\le 5\times 10^4 n4×104,m5×104

既然要维护区间众数,那么我们需要考虑和知晓的肯定是各个区间里各个数的出现的次数

于是进行分块,对于每一个块,求出一个 s u m sum sum数组, s u m [ i ] [ j ] sum[i][j] sum[i][j]表示在前 i i i个块中数 j j j的出现次数,下一步,我们需要统计出答案 m x mx mx数组, m x [ i ] [ j ] mx[i][j] mx[i][j]表示第 i i i块到第 j j j块的区间众数。因为有了 s u m sum sum数组,我们可以静态的枚举块,若块长为 n \sqrt{n} n ,则有 O ( n ) O(n) O(n)个区间,我们的问题就是如何 O ( n n ) O(n\sqrt{n}) O(nn )地预处理出 m x mx mx了,可以如此设想:假若我们处理出了 m x [ i ] [ j ] mx[i][j] mx[i][j],求 m x [ i ] [ j + 1 ] mx[i][j+1] mx[i][j+1]的时候,我们可以扫描第 j + 1 j+1 j+1个块,当扫描到数 x x x的时候,利用 s u m sum sum数组判断是否可以更新区间众数即可,我们就完成了 n \sqrt{n} n 时间内的状态转移,总计需要 O ( n n ) O(n\sqrt{n}) O(nn )的时间

下一步,我们应该考虑如何处理询问
可以采用二次扫描法,即对于区间 [ l , r ] [l,r] [l,r]来说,我们可以利用 m x 和 s u m mx和sum mxsum,然后对这个大块的两端零散部分,直接用这两个数组进行扫描,对于 m x mx mx的更新操作类比预处理时即可,在得出答案之后,再重新倒过来执行一次过程,将修改操作撤销,就可以了。
在实际代码中,为了消去撤销操作,可以单独建立数组 t o t tot tot处理两端,为了代码的方便懒惰,在统计mx的时候可以建立一个一维数组 c n t cnt cnt,在枚举块起点的时候即可进行统计修改

int q,n,m,block,siz,sum[205][40005],vis[40005],a[40005],b[40005],c[40005];
int tot[40005],cnt[40005],last;
struct node {
    int num,s;
}p[205][205];
int get(int x){
    return x%block==0?x/block:x/block+1;
}
inline void init(){
	scanf("%d%d",&n,&m);
	block=sqrt(n),siz=n%block==0?n/block:n/block+1;
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]),c[i]=a[i];
    sort(a+1,a+n+1);
    int q=unique(a+1,a+n+1)-a-1;
    for(int i=1;i<=n;i++){
    	int pos=lower_bound(a+1,a+q+1,c[i])-a;
    	b[pos]=c[i];
    	c[i]=pos;
	}
    for(int i=1;i<=siz;i++){
        memset(cnt,0,sizeof(cnt));
		node x;
        x.num=x.s=0;
        for(int j=i;j<=siz;j++){
            for(int k=(j-1)* block+1;k<=min(n,j * block);k++){
                cnt[c[k]]++;
                if((cnt[c[k]]>x.s)||(cnt[c[k]]==x.s&&c[k]<x.num)){
                    x.num=c[k];
                    x.s=cnt[c[k]];
                }
            }
            p[i][j]=x;
        }
    }
    for(int i=1;i<=siz;i++){
        for(int j=1;j<=q;j++)sum[i][j]=sum[i-1][j];
        for(int j=(i-1)* block+1;j<=min(n,i * block);j++)sum[i][c[j]]++;
    }
}
int solve(int l,int r){
	int ans=0;
	int L=get(l),R=get(r);
        if(R-L<=2){//优雅暴力 
        for(int j=l;j<=r;j++)tot[c[j]]=0;
        for(int j=l;j<=r;j++){
            tot[c[j]]++;
            if(tot[c[j]]>tot[ans]||(tot[c[j]]==tot[ans]&&ans>c[j]))ans=c[j];
        }
    } 
    else {
        ans=p[L+1][R-1].num;
    	tot[ans]=0,vis[ans]=0;
        for(int j=l;j<=min(n,L * block);j++)tot[c[j]]=0,vis[c[j]]=0;
        for(int j=(R-1)* block+1;j<=r;j++)tot[c[j]]=0,vis[c[j]]=0;
        for(int j=l;j<=min(n,L * block);j++)tot[c[j]]++;
        for(int j=(R-1)* block+1;j<=r;j++)tot[c[j]]++;
        int id,mx=0;
        for(int j=l;j<=min(n,L * block);j++){
            if(!vis[c[j]]){
                vis[c[j]]=1;
                int val=tot[c[j]]+sum[R-1][c[j]]-sum[L][c[j]];
                if(mx < val||(mx==val&&id>c[j]))mx=val,id=c[j];
            }
        }
        for(int j=(R-1)* block+1;j<=r;j++){
            if(!vis[c[j]]){
                vis[c[j]]=1;
                int val=tot[c[j]]+sum[R-1][c[j]]-sum[L][c[j]];
                if(mx < val||(mx==val&&id>c[j]))mx=val,id=c[j];
            }
        }
        if(mx>tot[ans]+p[L+1][R-1].s||(mx==tot[ans]+p[L+1][R-1].s&&ans>id))ans=id;
    }
    last=b[ans];
    return last;
}
int main(){
    init();
    for(int i=1;i<=m;i++){
        int l,r;scanf("%d%d",&l,&r);
        l=(l+last-1)%n+1;
        r=(r+last-1)%n+1;
        if(l>r)swap(l,r);
        printf("%d\n",solve(l,r));
    }
    return 0;
}

其实对于本题,还有另外一种方法维护某个区间内某个数的出现次数,也即我门需要的 c n t cnt cnt,就是对每一个数开一个 v e c t o r vector vector,;里面存入每一个数在序列中每一次出现的位置(有序),然后对于查询 [ l , r ] [l,r] [l,r]中数出现的次数只需要二分查找然后将下标相减即可
作诗:静态维护区间出现正偶次数的数的个数,强制在线
这道题是上道题的一个变式
类似的,我们进行分块, c n t [ i ] [ j ] cnt[i][j] cnt[i][j]表示第 i i i块开始到结尾, j j j的出现次数;

f [ i ] [ j ] f[i][j] f[i][j]表示 i i i块开头到 j j j块末尾的答案;

int get(int x){
	return x%block?x/block:x/block+1;
}
//预处理部分
	for (int i = 1; i <= siz; ++i) {
		int t = 0;
		for (int j = (i-1)*block+1; j <= n; ++j) {
			cnt[i][a[j]]++;
			if ((cnt[i][a[j]] & 1) && (cnt[i][a[j]] > 1)) t--;
			else if ((cnt[i][a[j]] & 1) == 0) t++;
			if (j%block==0) {
				f[i][j/block] = t;
			}
		}
	}

然后在统计答案的时候再新建立一个数组 t o t tot tot进行计数,记录两端的小块的数的出现次数,然后顺带就统计出这个数的出现次数进行分类讨论统计即可

#include<bits/stdc++.h>
using namespace std;
inline int read_int(){//快读
	int x=0;char c=getchar();
	while(c<'0'||c>'9')c=getchar();
	while(c>='0'&&c<='9'){
		x=(x<<3)+(x<<1)+(c-'0');
		c=getchar();
	}
	return x;
}
int a[100005],num[350][100005];//块的专用桶,也就是上文的cnt
int ans[350][350];//从i到j的答案
int n,m,c;//意义如题
int lb,nb;//块长与块个数
int l,r;//意义如题
int ll[350],rr[350],mm[100005];//块的左端点、右端点及单点所在块//这样避免了get函数,常数会更小一点
inline int number(int l,int r,int x){//块l到块r中x的个数
	return num[r][x]-num[l-1][x];
}
int A;//答案
int tong[100005];//临时桶,也即上文的tot
void solve(){
	A=0;
	int lb=mm[l],rb=mm[r];
	if(rb-lb<=1){//没有整块,直接暴力
		for(int i=l;i<=r;++i){
			++tong[a[i]];
			if(tong[a[i]]%2==0){
				++A;
			}else if(tong[a[i]]!=1){
				--A;
			}
		}
        //临时桶还原
		for(int i=l;i<=r;++i){
			--tong[a[i]];
		}
	}else{//一般情况
		A=ans[lb+1][rb-1];//整块的答案
        //左侧零散块暴力
		for(int i=l;i<=rr[lb];++i){
			++tong[a[i]];
			if((tong[a[i]]+number(lb+1,rb-1,a[i]))%2==0){
				++A;
			}else if(tong[a[i]]+number(lb+1,rb-1,a[i])!=1){
				--A;
			}
		}
        //右侧零散块暴力
		for(int i=ll[rb];i<=r;++i){
			++tong[a[i]];
			if((tong[a[i]]+number(lb+1,rb-1,a[i]))%2==0){
				++A;
			}else if(tong[a[i]]+number(lb+1,rb-1,a[i])!=1){
				--A;
			}
		}
        //临时桶还原
		for(int i=l;i<=rr[lb];++i){
			--tong[a[i]];
		}
		for(int i=ll[rb];i<=r;++i){
			--tong[a[i]];
		}
	}
}

int main(){
	n=read_int();c=read_int();m=read_int();
	lb=sqrt(n);nb=n/lb;if(lb*nb<n)++nb;//块长与块个数
    //预处理
	for(int i=1;i<=nb;++i){//块端点预处理
		ll[i]=(i-1)*lb+1;
		rr[i]=(i<nb)?i*lb:n;
	}
	for(int i=1;i<=nb;++i){
		for(int j=ll[i];j<=rr[i];++j){
			a[j]=read_int();
			mm[j]=i;//点所在块预处理
			++num[i][a[j]];//桶预处理
		}
		for(int j=0;j<=c;++j){//前缀和
			num[i][j]+=num[i-1][j];
		}
	}
	for(int i=1;i<=nb;++i){//整块答案预处理
		for(int j=i;j<=nb;++j){
			ans[i][j]=ans[i][j-1];
			for(int k=ll[j];k<=rr[j];++k){
				++tong[a[k]];
				if(tong[a[k]]%2==0){
					++ans[i][j];
				}else if(tong[a[k]]!=1){
					--ans[i][j];
				}
			}
		}
        //临时桶还原
		for(int j=0;j<=c;++j)tong[j]=0;
	}
	
    //解决
	while(~--m){
		l=read_int();r=read_int();
		l=(l+A)%n+1;r=(r+A)%n+1;
		if(l>r)swap(l,r);//解密
		solve();
		printf("%d\n",A);
	}
	
	return 0;
}

分块算法还有很多扩展,详见Ynoi
有时候遇到某些较为复杂的问题,甚至可以对分块算法进行扩展,比如先对序列进行分块,然后对于每一个块又进行值域分块

暴力骗分的分块变形:莫队算法

莫队算法的思想大概是这样的,对询问进行分块,必须离线操作,充分利用历史上的答案
基于分块思想,且 [ l , r ] [l,r] [l,r]的答案向 [ l , r + 1 ] , [ l , r − 1 ] , [ l − 1 , r ] , [ l + 1 , r ] [l,r+1],[l,r-1],[l-1,r],[l+1,r] [l,r+1],[l,r1],[l1,r],[l+1,r]四个相邻状态任意一个转移都是 O ( 1 ) O(1) O(1)
离线思想,将读入排序,按 n \sqrt{n} n 分块,每 n \sqrt{n} n 个为一块
排序时,将左端点块号相同的询问放在一个块里,右端点按单增排
对于每个块内相邻的两个询问,每次左右端点移动不会超过 n \sqrt{n} n ,是 O ( n ) O(\sqrt{n}) O(n )
块与块间相邻的两个询问,每次左右端点移动不会超过 2 n 2\sqrt{n} 2n ,也是 O ( s q r t n ) O(sqrt{n}) O(sqrtn)
试想以长度x为一块, [ 1 , x ] [ x + 1 , 2 x ] [1,x][x+1,2x] [1,x][x+1,2x],最坏就是 [ 1 , 1 ] − > [ 2 x , 2 x ] [1,1]->[2x,2x] [1,1]>[2x,2x]
因此,对于 m m m个询问,复杂度为 O ( m n ) O(m\sqrt{n}) O(mn )
本来可以通过将 [ l , r ] [l,r] [l,r]转化为平面坐标 ( l , r ) (l,r) (l,r),通过求曼哈顿最小生成树使得转移和最小,
但是,最坏情况下,曼哈顿最小生成树的复杂度和莫队分块的复杂度是一样的,
相比之下,莫队还好写,所以往往采用莫队算法来写
实现时,分块和下标移动套板子,魔改add函数即可
对于排序询问这个有一个小优化,如果l所在块的编号为奇数则按r升序排序,偶数则按r降序排序。
模板

//排序
inline bool cmp(query a,query b){
    return a.bl!=b.bl?a.l<b.l:((a.bl&1)?a.r<b.r:a.r>b.r);
}
//处理问题
	l=ask[1].l,r=ask[1].r;
	……//暴力计算答案
    for(int i=2;i<=m;i++){
        while (l<ask[i].l) del(c[l++]);
        while (l>ask[i].l) add(c[--l]);
        while (r<ask[i].r) add(c[++r]);
        while (r>ask[i].r) del(c[r--]);//将答案移动至指定范围
      	ans[ask[i].id]=……;
    }

大概流程是这样的:
算法大概遵循一个这样的流程:

  1. 对于所有区间端点的移动,我们要设计出一种 O ( 1 ) O(1) O(1)的方法使得我们可以快速维护移动端点后一个区间的答案。
  2. 有了这种方法之后,我们根据刚才的复杂度分析,我们对整个序列分块,每一块大小 O ( n ) O(\sqrt{n}) O(n )
  3. 然后我们对所有询问的区间排序,排序完之后左端点每一次最多移动 n \sqrt{n} n 的距离总共 n n n 次,右端点单调不降所以每一个块移动 n n n 的距离总共 n \sqrt{n} n 次,所以总复杂度为 O ( n n ) O(n\sqrt{n}) O(nn )
    下面一个很简单的莫队应用,给定 m m m次询问,每次询问 [ l , r ] [l,r] [l,r]中数 x x x的出现次数
int block,ans[100005],siz,pos[100005],a[10005],cnt[100005],n,m;
struct node{
	int l,r,x,id;
}ask[100050];
void add(int x){
	cnt[x]++;
}
void del(int x){
	cnt[x]--;
}
bool cmp(node a,node b){
	return pos[a.l]==pos[b.l]?(pos[a.l]&1?a.r<b.r:a.r>b.r):pos[a.l]<pos[a.r];
} 
int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;i++)scanf("%d",&a[i]);
	block=sqrt(n),siz=n%block?n/block+1:block;
	for(int i=1;i<=siz;i++){
		int l=(i-1)*block+1,r=min(n,block*i);
		for(int j=l;j<=r;j++)pos[j]=i;
	}
	for(int i=1;i<=m;i++){
		scanf("%d%d%d",ask[i].l,ask[i].r,ask[i].x);
		ask[i].id=i;
	}
	sort(ask+1,ask+m+1,cmp);
	int l=ask[1].l,r=ask[1].r;
	for(int i=l;i<=r;i++)add(a[i]);
	ans[ask[1].id]=cnt[ask[i].x];
	for(int i=2;i<=m;i++){
		while(l>ask[i].l)add(a[--l]);
		while(l<aks[i].l)del(a[l++]);
		while(r<ask[i].r)add(a[++r]);
		while(r>ask[i].r)del(a[r--]);
		ans[ask[i].id]=cnt[ask[i].x];
	}
	for(int i=1;i<=m;i++)printf("%d\n",ans[i]);
}

当然这道题也可以用上文所述的朴素分块的 O ( n n ) O(n\sqrt{n}) O(nn )或者 v e c t o r vector vector解法(更加优秀)

总而言之,分块就是运用了其大段维护,小段朴素和其常数优势

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值