分块

写在前面

非常简单的分块,使我开心的转圈,稍微看了一下 oi wiki 就懂了,妈妈再也不用担心我的暴力了

正文

分块,非常优雅的暴力,本质是上是通过对原数据的适当划分,并在划分后的每一个块上预处理部分信息,从而较一般的暴力算法取得更优的时间复杂度。
例如(博客萌新好不容易整的)

a1+a2++ask1as+1+as+2++a2sk2a(s+1)×s+1++ankns

n 如果不是s的倍数最后一个块可能不完整,但也没关系,对最后操作无影响

查询操作

如果负责查询的 l,r 都在同一个块中,暴力枚举 lr
如果 lr 不在所在块的头或尾,则暴力将 lr 枚举到块的头或尾部
然后再从 l 所在的整块枚举整快到 r 所在的整块,将所有查询结果加和,即为结果

修改操作

和查询操作的思路一样
就是在整块修改时,无法依次修改块中元素,需要在修改时打个 add[] 标记,存储整块 k[] 的值被修改了,而单点 a[] 却没有被修改的数值,在查询单点 a[] 时加上标记就行

警示后人

在判断第 i 个数属于哪个块时,要注意对于任意一个块 k[j] (为了简单举 j=1 的例子)的下标为 1,2,3,s
然而 1,2,3,s1 的下标 i 进行对应块的下标却为 is+1 ,要记得特判

升级版分块

P2801 教主的魔法

就是多了一个区间查询大于等于 k 的个数
考虑对于一个整块,我们每次查询时,若它是无序的则排一个序,然后二分查找第一个大于等于的数就行了

代码

属于是一个失败,最后都不知道自己在写什么了,还是伟大的雪猫学长调了半个小时,找到了无数个锅之后,才总算AC了,啊,伟大%%%!!!

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
char op;
int s,n,q,cnt,l,r,w;
int k[N],a[N],b[N],add[N];
int id(int x){
	if(x%s==0)  return x/s;
	return x/s+1;
}
void change(int x,int y,int ad){
	if(id(x)==id(y)){
		for(int i=x;i<=y;i++){
			a[i]+=ad;
		}
		k[id(x)]=1;
		return;
	}
	while(x%s!=1){
		a[x]+=ad; 
		k[id(x)]=1;
		x++;
	}
	while(y%s!=0){
		a[y]+=ad;
		k[id(y)]=1;
		y--;//锅1,y减小不是增大 
	}
	for(int i=id(x);i<=id(y);i++){
		add[i]+=ad;
	}
}
int query(int x,int y,int c){
	int ans=0;
	if(id(x)==id(y)){
		for(int i=x;i<=y;i++){
			if(a[i]+add[id(x)]>=c){
				ans++;
			}
		}
		return ans;
	}
	while(x%s!=1){
		if(a[x]+add[id(x)]>=c){
			ans++;
		}
		x++;
	}
	while(y%s!=0){
		if(a[y]+add[id(y)]>=c){
			ans++;
		}
		y--;//锅2,同锅1 
	}
	for(int i=id(x);i<=id(y);i++){
		if(k[i]){
			for(int j=(i-1)*s+1;j<=min(i*s,n);j++){
				b[j]=a[j];
			}
			sort(b+(i-1)*s+1,b+min(i*s+1,n+1));//锅3,sort起始位+1不是+2,考虑当i=1时值为1
			//锅4,考虑越界 
			k[i]=0;
		}
		ans+=min(n+1,i*s+1)-(lower_bound(b+(i-1)*s+1,b+min(i*s+1,n+1),c-add[i])-b);//锅5,考虑lower_bound的用法 
	}
	return ans;
}
int main(){
	scanf("%d%d",&n,&q);
	s=(int)sqrt(n);
	for(int i=1;i<=n;i++){
		scanf("%d",&a[i]);
		if(i%s==1)  cnt++; 
		b[i]=a[i];
	}
	for(int i=1;i<=cnt;i++)  k[i]=1;
	for(int i=1;i<=q;i++){
		scanf(" %c %d%d%d",&op,&l,&r,&w);
		if(op=='M'){
			change(l,r,w);
		}
		else{
			printf("%d\n",query(l,r,w));
		}
	}
}

ps:更新2024.12.28

本人重学分块,之前写的代码非常不优雅,于是又写了一篇很优雅的代码

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5,M=1e4+5;
int n,q,block,t;
int st[M],en[M],add[M],pos[N];
struct nm{
    int a,id;
}a[N];
bool cmp(nm x,nm y){
    return x.a<y.a;
}
void merge(int l,int r,int w){
    int p=pos[l],q=pos[r];
    if(p==q){
        for(int i=st[p];i<=en[q];i++){
            if(a[i].id<=r&&l<=a[i].id){
                a[i].a+=w;
            }
        }
        sort(a+st[p],a+en[q]+1,cmp);
    }
    else{
        for(int i=p+1;i<=q-1;i++)  add[i]+=w;
        for(int i=st[p];i<=en[p];i++){
            if(a[i].id<=r&&l<=a[i].id){
                a[i].a+=w;
            }
        }
        sort(a+st[p],a+en[p]+1,cmp);
        for(int i=st[q];i<=en[q];i++){
            if(a[i].id<=r&&l<=a[i].id){
                a[i].a+=w;
            }
        }
        sort(a+st[q],a+en[q]+1,cmp);
    }
}
int dic(int x,int y,int c){
    if(a[y].a<c)  return 0;
    int l=x,r=y;
    while(l<r){
        int mid=(l+r)>>1;
        if(a[mid].a<c)  l=mid+1;
        else  r=mid;
    }
    return y-l+1;
}
int query(int l,int r,int w){
    int p=pos[l],q=pos[r],res=0;
    if(p==q){
        for(int i=st[p];i<=en[p];i++){
            if(a[i].id<=r&&l<=a[i].id&&a[i].a>=w-add[p])  res++;
        }
    }
    else{
        for(int i=p+1;i<=q-1;i++){
            res+=dic(st[i],en[i],w-add[i]);
        }
        for(int i=st[p];i<=en[p];i++){
            if(a[i].id<=r&&l<=a[i].id&&a[i].a>=w-add[p])  res++;
        }
        for(int i=st[q];i<=en[q];i++){
            if(a[i].id<=r&&l<=a[i].id&&a[i].a>=w-add[q])  res++;
        }
    }
    return res;
}
int main(){
    scanf("%d%d",&n,&q);
    block=sqrt(n);
    t=n/block;
    if(n%block)  t++;
    for(int i=1;i<=t;i++){
        st[i]=(i-1)*block+1;
        en[i]=i*block;
    }
    en[t]=n;
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i].a);
        a[i].id=i;
        pos[i]=(i-1)/block+1;
    }
    for(int i=1;i<=t;i++){
        sort(a+st[i],a+en[i]+1,cmp);
    }
    for(int i=1;i<=q;i++){
        char c[1];
        int l,r,w;
        scanf("%s%d%d%d",c,&l,&r,&w);
        if(c[0]=='M'){
            merge(l,r,w);
        }
        else{
            printf("%d\n",query(l,r,w));
        }
    }
}

hdu5057

分块直接做就完了

按照每一位维护,然后就是分块板子

代码(没有测试,只通过了样例

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5,M=400;
int n,m,T,block,t;
int a[10][N],num[10][10][M],pos[N],st[M],en[M];
void build(int x,int y){
    for(int i=0;i<=9;i++){
        int now=y%10;
        y/=10;a[i][x]=now;
        num[i][now][pos[x]]++;
    }
}
void change(int x,int y){
    for(int i=0;i<=9;i++){
        int old=a[i][x],now=y%10;
        y/=10;a[i][x]=now;
        num[i][old][pos[x]]--;
        num[i][now][pos[x]]++;
    }
}
int query(int l,int r,int d,int g){
    int p=pos[l],q=pos[r],res=0;
    if(p==q){
        for(int i=l;i<=r;i++){
            if(a[d][i]==g)  res++;
        }
    }
    else{
        for(int i=l;i<=en[p];i++){
            if(a[d][i]==g)  res++;
        }
        for(int i=st[q];i<=r;i++){
            if(a[d][i]==g)  res++;
        }
        for(int i=p+1;i<=q-1;i++){
            res+=num[d][g][i];
        }
    }
    return res;
}
int main(){
    scanf("%d",&T);
    while(T--){
        scanf("%d%d",&n,&m);
        block=(int)sqrt(n);
        t=n/block;
        if(n%block)  t++;
        for(int i=1;i<=n;i++){
            pos[i]=(i-1)/block+1;
        }
        for(int i=1;i<=t;i++){
            st[i]=(i-1)*block+1;
            en[i]=i*block;
        }
        en[t]=n;
        for(int i=0;i<=9;i++){
            for(int j=0;j<=9;j++){
                for(int k=1;k<=t;k++){
                    num[i][j][k]=0;
                }
            }
        }
        for(int i=0;i<=9;i++){
            for(int j=1;j<=n;j++){
                a[i][j]=0;
            }
        }
        for(int i=1;i<=n;i++){
            int g;
            scanf("%d",&g);
            build(i,g);
        }
        for(int i=1;i<=m;i++){
            char c[1];
            int l,r,d,g;
            scanf("%s",c);
            if(c[0]=='Q'){
                scanf("%d%d%d%d",&l,&r,&d,&g);
                printf("%d\n",query(l,r,d-1,g));
            }
            else{
                scanf("%d%d",&l,&g);
                change(l,g);
            }
        }
    }
}

P3203 弹飞绵羊

非常巧妙的转化

我们先考虑如暴力做的话如果查询 O(1) 修改就 O(n),修改 O(1) 查询 O(n),然后所以如果可以将两个操作都下降到 O(n),就可以解决了

所以考虑分块,我们对一个块内部做到修改 O(block),查询O(1)(block为块长)

这样我们就可以查询时跳块,做到 O(t) (t为块个数)

因为分块 t,block 都是 O(n) 量级的,所以就可以直接做到 O(mn)

实现:

num[i] 表示绵羊从 i 跳出它所在的块所需要的弹跳次数,然后 to[i] 表示从 i 跳出它所在的块落到其它块的点

然后修改时对于整个块重新统计贡献,查询时一个一个往后跳

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,M=505;
int n,m,block,t;
int to[N],num[N],st[M],en[M],k[N],pos[N];
int query(int g){
    int nxt=g,res=0;
    while(nxt<=n){
        res+=num[nxt];
        nxt=to[nxt];
    }
    return res;
}
void opera(int i){
    for(int j=en[i];j>=st[i];j--){
        if(j+k[j]>en[i]){
            num[j]=1;
            to[j]=j+k[j];
        }
        else{
            num[j]=num[j+k[j]]+1;
            to[j]=to[j+k[j]];
        }
    }
}
void change(int x,int g){
    k[x]=g;
    int i=pos[x];
    opera(i);
}
int main(){
    scanf("%d",&n);
    block=(int)sqrt(n);
    t=n/block;
    if(n%block)  t++;
    for(int i=1;i<=n;i++){
        pos[i]=(i-1)/block+1;
    }
    for(int i=1;i<=t;i++){
        st[i]=(i-1)*block+1;
        en[i]=i*block;
    }
    en[t]=n;
    for(int i=1;i<=n;i++){
        scanf("%d",&k[i]);
    }
    for(int i=1;i<=t;i++){
        opera(i);
    }
    scanf("%d",&m);
    for(int i=1;i<=m;i++){
        int op,q,k;
        scanf("%d",&op);
        if(op==1){
            scanf("%d",&q);
            q++;
            printf("%d\n",query(q));
        }
        else{
            scanf("%d%d",&q,&k);
            q++;
            change(q,k);
        }
    }
}

loj6279

给出一个长为n的数列,以及n个操作,操作涉及区间加法,询问区间内小于某个值x的前驱(比其小的最大元素)。

和正常的教主的魔法差不多,然后就转化一下就可以

loj6282

给出一个长为n的数列,以及n个操作,操作涉及单点插入,单点询问,数据随机生成。

因为是随机数据,可以直接做,在块里暴力插入,块与块之间暴力挨个找寻找下标

考虑不是随机数据怎么办,因为数据不随机,所以可以出现一直插在一个块中的情况,会炸

所以我们每插入 O(n) 次,就重新平均分一下块,复杂度不会炸因为最多进行 O(n) 次,每次操作复杂度 O(n)

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+5,M=500;
int n,block,t,q;
int pos[N],a[N],en[M],num[M][N];
void allocate(){
    block=(int)sqrt(n);
    t=n/block;
    if(n%block)  t++;
    for(int i=1;i<=t;i++){
        en[i]=block;
    }
    if(n%block)  en[t]=n%block;
    for(int i=1;i<=n;i++){
        pos[i]=(i-1)/block+1;
    }
    int id=1;
    for(int i=1;i<=n;i++){
        int p=pos[i];
        num[p][id]=a[i];
        id++;
        if(i%block==0){
            id=1;
        }
    }
}
void insert(int x,int r){
    int now=0,p=0;
    while(now<x){
        p++;
        now+=en[p];
    }
    now-=en[p];
    int id=x-now;
    // printf("%d %d\n",p,id);
    for(int i=now+en[p];i>=id;i--){
        num[p][i+1]=num[p][i];
    }
    num[p][id]=r;
    en[p]++;
}
int query(int x){
    int now=0,p=0;
    while(now<x){
        p++;
        now+=en[p];
    }
    now-=en[p];
    int id=x-now;
    // printf("%d %d\n",p,id);
    return num[p][id];
}
void remerge(){
    n=0;
    for(int i=1;i<=t;i++){
        for(int j=1;j<=en[i];j++){
            a[++n]=num[i][j];
        }
    }
}
int main(){
    scanf("%d",&n);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    allocate();
    // for(int i=1;i<=n;i++){
    //     printf("%d ",pos[i]);
    // }
    // printf("\n");
    q=n;
    int re=sqrt(n);
    for(int i=1;i<=q;i++){
        int op,l,r,c;
        scanf("%d%d%d%d",&op,&l,&r,&c);
        if(op==0){
            insert(l,r);
        }
        else{
            printf("%d\n",query(r));
        }
        // for(int i=1;i<=t;i++){
        //     for(int j=1;j<=en[i];j++){
        //         printf("%d ",num[i][j]);
        //     }
        //     printf("\n");
        // }
        // printf("\n");
        if(i%re==0){
            remerge();
            allocate();
        }
    }
}

P4145 上帝造题的七分钟 2 / 花神游历各国

首先观察到一个性质,就是一个数至多被开根6次就会变为0/1,所以我们只要对哪些不是0/1的块暴力修改,然后如果全都是0/1就跳过,复杂度一定是正确的

loj6284

也是一样的观察性质,一整块被改为1个数后我们就可以统一操作了,然后考虑散块怎么办,如果暴力修改的话,最劣复杂度是 O(n)

但是我们考虑对于一次修改操作,它最多只会破坏两个完整的块,也就是我们暴力修改的块的总数不会超过 3n 个,复杂度正确

P4168 [Violet] 蒲公英

非常巧妙的题,使我的大脑旋转

我们首先先对数列分块

然后我们考虑整块与散块的关系,因为我们想要知道整块要维护些什么,所以我们要知道众数的求法和整块散块的关系

一次区间查询操作会查到左右两边的散块,还有中间若干整块

考虑当众数在只出现在整块中的情况,那我们可以直接预处理得出(就是预处理出 mode[i][j] 表示在 i j 之间的整块的众数),具体实现看我以下 preprocess 函数

若众数在散块中也出现过呢?

那么显然根据分块思想,我们将散块中所有出现过的值的出现次数与答案比较,然后更新答案即可

怎么算一个值出现的次数:

我们开个桶,表示这个值在散块中的出现次数,然后可以预处理出 s[i][j] 表示在 1 i 的整块中出现了多少次 j,然后前缀和计算,一个值出现的次数就是桶中的次数+ s[p+1][q1]

因为我们要把值存在数组里,所以要离散化一下,然后还有输出时记得再把离散完的赋值回去(这个锅调了好久)

代码:

点击查看代码
#include<bits/stdc++.h>
using namespace std;
const int N=4e4+5,M=205;
int n,m,cnt,block,t;
int a[N],b[N],en[N],st[N],pos[N],mode[M][M],s[M][N],num[M][N],bar[N],id[N];
map<int,int>mp;
void dis(){
    sort(b+1,b+1+n);
    cnt=unique(b+1,b+1+n)-b-1;
    for(int i=1;i<=cnt;i++){
        mp[b[i]]=i;
        id[i]=b[i];
    }
    for(int i=1;i<=n;i++){
        a[i]=mp[a[i]];
    }
}
void allocate(){
    block=(int)sqrt(n);
    t=n/block;
    if(n%block)  t++;
    for(int i=1;i<=t;i++){
        st[i]=(i-1)*block+1;
        en[i]=i*block;
    }
    en[t]=n;
    for(int i=1;i<=n;i++){
        pos[i]=(i-1)/block+1;
    }
}
void preprocess(){
    for(int i=1;i<=t;i++){
        for(int j=st[i];j<=en[i];j++){
            num[i][a[j]]++;
        }
    }
    for(int i=1;i<=t;i++){
        for(int j=1;j<=cnt;j++){
            s[i][j]=s[i-1][j]+num[i][j];
        }
    }
    for(int i=1;i<=t;i++){
        for(int j=i;j<=t;j++){
            mode[i][j]=mode[i][j-1];
            for(int k=st[j];k<=en[j];k++){
                bar[a[k]]++;
                if(bar[a[k]]>bar[mode[i][j]]||(bar[a[k]]==bar[mode[i][j]]&&a[k]<mode[i][j]))  mode[i][j]=a[k];
            }
        }
        for(int j=0;j<=cnt;j++){
            bar[j]=0;
        }
    }
}
bool compare(int x,int y,int p,int q){
    int numx=bar[x]+s[q-1][x]-s[p][x],numy=bar[y]+s[q-1][y]-s[p][y];
    if(numx>numy||(numx==numy&&x<y))  return 1;
    return 0;
}
int query(int l,int r){
    int p=pos[l],q=pos[r],res=0;
    if(p==q){
        for(int i=l;i<=r;i++){
            bar[a[i]]++;
            if(bar[a[i]]>bar[res]||(bar[a[i]]==bar[res]&&a[i]<res))  res=a[i];
        }
        for(int i=l;i<=r;i++)  bar[a[i]]=0;
    }
    else{
        res=mode[p+1][q-1];
        for(int i=l;i<=en[p];i++){
            bar[a[i]]++;
            if(compare(a[i],res,p,q))  res=a[i];
        }
        for(int i=st[q];i<=r;i++){
            bar[a[i]]++;
            if(compare(a[i],res,p,q))  res=a[i];
        }
        for(int i=l;i<=en[p];i++)  bar[a[i]]=0;
        for(int i=st[q];i<=r;i++)  bar[a[i]]=0;
    }
    return res;
}
int main(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        b[i]=a[i];
    }
    dis();
    allocate();
    preprocess();
    int x=0;
    for(int i=1;i<=m;i++){
        int l,r;
        scanf("%d%d",&l,&r);
        l=(l+x-1)%n+1;
        r=(r+x-1)%n+1;
        if(l>r)  swap(l,r);
        x=id[query(l,r)];
        printf("%d\n",x);
    }
}

参考

「分块」数列分块入门1 – 9 by hzwer

P3870

我们维护一个add表示块整体进行翻转了几次,然后散块暴力反转翻转即可

P3396

想了好久,终于切了

根号分治题(话说不应该放在分块板块中)

对于模数在根号以内的数,我们暴力处理是 O(n) 的,无法接受,所以选择预处理 f[i][j] 表示当模数为 i 时,余数为 j 时的总和

对于模数在根号以外的数,就暴力查询

至于修改操作,只用修改根号以内的模数预处理出的答案即可

复杂度在 O((n+m)n) 之内

P3863

非常巧妙的一题,首先我们考虑当序列中只有一个数的时候应该怎么办

我们可以对时间分一下块,然后加x就是将区间 [t,q](t为当前时间)加上x

查询就是查询 [0,t) 的区间中有多少个数不小于x

然而现在又有了第二层的区间限制,怎么办呢?

考虑扫描线思想,我们从小到大枚举序列中的每个元素,考虑一次区间修改操作[l,r],就在处理l的时候将 [t,q] 加上x,在处理r+1时 [t,q] 减去x

也就是我们维护一个承载着修改操作的分块序列,对于一次查询,我们需要在查询时将所有数加上这一位的初始值(不是真加,形式上的加,为了在传递到下一位序列时不受影响)

然后就做完了

P1975

水紫,想到了一个听起来很正确的解法(没有验证),也算切了吧

我的思路是

应该是很正确的,复杂度也没有问题

然后正解做法更优雅,所以就写的正解

我们交换 x,y 那么对于 1 x1,y+1 n 是没有影响的

所以我们只需要查询一下交换对 x+1,y1 的影响

还要注意 (x,y) 逆序对带来的影响,注:特判 ax==ay

还有就是注意要保证 x<y

posted @   daydreamer_zcxnb  阅读(38)  评论(3编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示