分块/莫队总结(未完结)

分块/莫队总结

分块是必须要写一下总结的了。(主要是再不写我就烂的没边了。。)

世界上怎么会有分块这种毒瘤的东西啊。要是我赛场上敢写分块说明可以把我送去医院了。(确信)

据说今年 SNOI 2022 赛场上用分块写 T2 的都挂的非常惨。。。(所以还不如写树状数组呢还有 45pts 学分块干嘛)

谈笑归谈笑,学还是必须要学的,掌握还是必须要掌握的,总结还是终归要总结的。

下面的内容可能是我花了一个晚上(通宵)的心血整理和总结的,请务必珍惜。(?)


基本思想

通过适当的划分,预处理出来一部分信息并保存下来,用空间换时间,达到时空平衡。

大部分情况下,都是将一个数列 AA 划分成大约 n\sqrt n 个长度不超过 n\sqrt n 的块,每次对于区间 [l,r][l,r] 进行操作时,可以先将整个区间划分为三部分:最左边独立的区间中间若干个整块最右边独立的区间,我们采用“大段维护,局部朴素“的思想,将首尾两个独立区间单独暴力求解,对于中间的若干块整块直接处理即可。

基本操作

区间加法 单点查询

题目链接

原 loj 数列分块入门 1

思路分析

将原数列划分为大约 n\sqrt n 个长度至多为 n\sqrt n 的若干个不同的块。预处理出来原数列中的每个位置 xx 所属块 idxid_x,以及每一个块 ii 的左右端点 lil_irir_i

对于区间加法:

假设执行该操作的区间是 [L,R][L,R],那么从大的方面将区间分为多个部分: [L,r[idL]],[l[idL+1],r[idL+1]],[l[idL+2,r[idL+2]],,[l[idR],R][L,r[id_L]],[l[id_{L+1}],r[id_{L+1}]],[l[id_{L+2},r[id_{L+2}]],\cdots,[l[id_R],R]

那么对于两边的两个部分可以直接暴力求解,直接对于每一个 ii 使得 ai+ca_i+c

对于中间的若干块,可以用一个类似于线段树的 lazy 标记记录一下要加的值,即对于每一个块 ii 使得 lazyi+clazy_i+c

对于单点查询:直接输出 ar+lazyidra_r+lazy_{id_r} 即可。

时间复杂度

O(nn)O(n\sqrt n)

代码实现

void ins(int L,int R,int c)
{
    int now1=L/sz+((L%sz)?1:0);
    int now2=R/sz+((R%sz)?1:0);
    if(now1==now2)
    {
        for(int i=L;i<=R;i++)a[i]+=c;
        return ;
    }
    else
    {
        if(L==l[now1])lazy[now1]+=c;
        else for(int i=L;i<=r[now1];i++)a[i]+=c;
        for(int i=now1+1;i<now2;i++)lazy[i]+=c;
        if(R==r[now2])lazy[now2]+=c;
        else for(int i=l[now2];i<=R;i++)a[i]+=c;
    }
    return ;
}

signed main()
{
    n=read();
    sz=sqrt(n);
    cnt=n/sz+((n%sz)?1:0);
    for(int i=1;i<=n;i++)
    {
        a[i]=read();
        id[i]=(i-1)/sz+1; 
    }
    for(int i=1;i<=cnt;i++)
    {
        l[i]=(i-1)*sz+1;
        r[i]=i*sz;
    }
    for(int i=1;i<=n;i++)
    {
        int opt,l,r,c;
        opt=read();l=read();r=read();c=read();
        if(opt==0)ins(l,r,c);
        else cout<<a[r]+lazy[id[r]]<<'\n';
    }
    return 0;
}

区间加法 区间计数(小于 xx

题目链接

原 loj 数列分块入门 2

思路分析

区间加法同上。

对于找区间内小于 xx 的个数:

我们同样可以采用“大段维护,局部朴素”的方法。假设每次查询的最后答案是 retret,将原区间划分为若干部分后,对于两边的两部分可以直接暴力判断,判断每个 ai+lazyidia_i+lazy_{id_i} 是否小于 cc,若小于则 ret++。对于中间若干块,我们可以把每一个块内的元素排序一下,然后二分求解。

但由于不能破坏原数列的相对位置,每次可以把原序列重载一遍,存到另一个数组里去,然后直接在另一个数组里排序和二分。

但是这就牵扯到一个问题:每次更新后,到底应该在哪里重载原数列呢?是在每次区间加法操作结束后还是每次查询操作开始前?

我们来分析一下。

假如在每次查询操作开始前重载,那么就要把区间 [l,r][l,r] 内所有的整块都重新重载一遍,因为你不知道这些整块是否更新过,所以只能老老实实的把所有的整块重载。看起来这个重载操作时间复杂度相当大,事实上我这么做也 T 成 30 了。。。

但是如果你在每次区间加法操作结束后重载,那么对于 [l,r][l,r] 内的整块,它们内部的每个元素间相对大小并不会改变,也就是说,你根本不需要重载这些整块,你只需要对于每一块 ii 给它们的整体加上 lazyilazy_i 即可,当查询时再把 lazyilazy_i 加回来。 这就发挥了 lazy 标记的妙用,完全就减少了对于那些整块要重载的时间复杂度。

那么我们每次只需要重载前后两个不是整块的部分重载一遍它们所在的块,注意!不是重载它们那一部分不是整块的区间,而是重载它们所在的块!因为是整个块的相对大小会发生改变!(我 TM 在这挂了至少四个小时)那么每次重载的长度最多是 2n2\sqrt n

易错点

之所以在这里专门加上易错点这一项,是因为我这道题当时从下午 3 点一直挂到晚上 11 点,最后还是找代老师调出来的,所以加上这一项让我印象更加深刻,对自己的错误也更加熟悉。

  • 开的数组大小要大于它本身的 5e45e4,最好多开一到两个 szsz 的大小。

  • 每次区间加操作结束后重载数列而非每次查询操作前

  • 初始化重载数组时应该按块排序而不是对数组整个排序

  • 每次排序前需要对块内的所有元素都复制到重载数组中,而非单纯的区间内元素

  • 排序的是每个块而非区间内元素。(所以这里我们可以直接写个重载函数用来重载和排序重载数组)

  • 由于我使用的是 aqx 二分(当然也可以用 lower_bound 但是不知道为什么我 lower_bound 的用法一直都不是很会),所以在每次判断时要注意数组下标不要越界否则会 RE。

  • 不要把字母写错……

时间复杂度

单次区间加:O(logn)O(\log n),单次查询:O(logn)O(\log \sqrt n)

总复杂度:O(nlogn+nlogn)O(n \log n+n \log \sqrt n)

代码实现

void upd(int now)
{
    for(int i=l[id[now]];i<=r[id[now]];i++)b[i]=a[i];//对整个块都重载+排序。
    sort(b+l[id[now]],b+r[id[now]]+1);
}

void ins(int L,int R,int c)
{
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)a[i]+=c;
        upd(L);//每次 ins 操作结束就重载。
        return ;
    }
    for(int i=L;i<=r[ID1];i++)a[i]+=c;
    upd(L);
    for(int i=ID1+1;i<=ID2-1;i++)lazy[i]+=c;//中间若干个整块不需要重载。
    for(int i=l[ID2];i<=R;i++)a[i]+=c;
    upd(R);
    return ;
}

int query(int L,int R,int c)
{
    int ret=0;
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)
        {
            if(a[i]+lazy[id[i]]<c)ret++;
        }
        return ret;
    }
    for(int i=L;i<=r[ID1];i++)if(a[i]+lazy[id[i]]<c)ret++;
    for(int i=l[ID2];i<=R;i++)if(a[i]+lazy[id[i]]<c)ret++;
    for(int i=ID1+1;i<=ID2-1;i++)
    {
        int now=0;
        for(int step=(1<<17);step>=1;step>>=1)
        {
            if(now+step<=(r[i]-l[i]+1)&&b[l[i]+now+step-1]+lazy[i]<c)now+=step;
            //这里数组大小不要越界。now+step 应该小于等于区间长度。并且数组下标是 l[i]+now+step-1,而不是 now+step.
        }
        ret+=now;
    }
    return ret;
}

int main()
{
    n=read();
    sz=sqrt(n);
    cnt=n/sz+((n%sz)?1:0);
    for(int i=1;i<=n;i++)
    {
        a[i]=read();
        b[i]=a[i];
        id[i]=(i-1)/sz+1;
    }
//    sort(b+1,b+n+1);//非整个数列排序
    for(int i=1;i<=cnt;i++)
    {
        l[i]=(i-1)*sz+1;
        r[i]=i*sz;
        sort(b+l[i],b+r[i]+1);//每个块排序
    }
    for(int i=1;i<=n;i++)
    {
        int opt,l,r,c;
        opt=read();l=read();r=read();c=read();
        if(opt==0)ins(l,r,c);
        else cout<<query(l,r,c*c)<<'\n';
    }
}

感觉我刚开始这道题的思想我就没完全理解,所以我写了一堆什么狗屁玩意。难怪挂那么久。。

区间加法 区间前驱查询(查 xx 的前驱)

题目链接

原 loj 数列分块入门 3

思路分析

首先要明确一个概念:什么叫前驱?

答:比其小的最大元素。

区间加法同上。

其实一看这个比它小的最大元素我们就应该想到二分,事实上思路和分块 2 的思想几乎一样,只需要在每次查询时维护的是当前小于 cc 的最大值即可。

也就是说,对于两边非整块区间,只需要将上面的 if(a[i]+lazy[id[i]]<c)ret++; 改成 if(a[i]+lazy[id[i]]<c)ret=max(a[i]+lazy[id[i]],ret);

然后在二分时,要判断当前块内最小值是否都大于 cc,如果不大于,那么直接 continue。千万要注意这一点十分重要,因为如果不大于 c 那么有可能用 0 来更新答案。这样很可能是错误的。

另外在查询操作时应该初始化答案为某一个到达不了的极小值,最好不要用 -1。(虽然本题 -1 可以过,但原数据范围的大小是 231-2^{31}2312^31,直接初始化 -1 有一定的出错几率。)

时间复杂度

同分块 2。

代码实现

void upd(int now)
{
    for(int i=l[id[now]];i<=r[id[now]];i++)b[i]=a[i];
    sort(b+l[id[now]],b+r[id[now]]+1);
}

void ins(int L,int R,int c)
{
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)a[i]+=c;
        upd(L);
        return ;
    }
    for(int i=L;i<=r[ID1];i++)a[i]+=c;
    upd(L);
    for(int i=ID1+1;i<=ID2-1;i++)lazy[i]+=c;
    for(int i=l[ID2];i<=R;i++)a[i]+=c;
    upd(R);
    return ;
}

int query(int L,int R,int c)
{
    int ret=-(1ll<<32);
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)
        {
            if(a[i]+lazy[id[i]]<c)ret=max(a[i]+lazy[id[i]],ret);
        }
        return ret;
    }
    for(int i=L;i<=r[ID1];i++)if(a[i]+lazy[id[i]]<c)ret=max(a[i]+lazy[id[i]],ret);
    for(int i=l[ID2];i<=R;i++)if(a[i]+lazy[id[i]]<c)ret=max(a[i]+lazy[id[i]],ret);
    for(int i=ID1+1;i<=ID2-1;i++)
    {
        int now=0;
        for(int step=(1<<17);step>=1;step>>=1)
        {
            if(now+step<=(r[i]-l[i]+1)&&b[l[i]+now+step-1]+lazy[i]<c)now+=step;
        }
        if(now)ret=max(b[l[i]+now-1]+lazy[i],ret);//这里判断 now!=0 就是为了防止块内最小值也大于 ret 从而用 0 更新答案的情况。
    }
    return ret;
}

signed main()
{
    n=read();
    sz=sqrt(n);
    cnt=n/sz+((n%sz)?1:0);
    for(int i=1;i<=n;i++)
    {
        a[i]=read();
        b[i]=a[i];
        id[i]=(i-1)/sz+1;
    }
//    sort(b+1,b+n+1);
    for(int i=1;i<=cnt;i++)
    {
        l[i]=(i-1)*sz+1;
        r[i]=i*sz;
        sort(b+l[i],b+r[i]+1);
    }
    for(int i=1;i<=n;i++)
    {
        int opt,l,r,c;
        opt=read();l=read();r=read();c=read();
        if(opt==0)ins(l,r,c);
        else
        {
            int ans=query(l,r,c);
            if(ans==-(1ll<<32))cout<<-1<<'\n';
            else cout<<ans<<'\n';
        }
    }
}

区间加法 区间求和

题目链接

原 loj 数列分块入门 4

思路分析

区间加法同上。

对于区间求和,我们可以维护一个前缀和数组 sumsum,在每次区间加操作时,只需要给整块 lazyi+c,sumi+c×szlazy_i+c,sum_i+c\times szszsz 就是 n\sqrt n),在每次求区间和时只需要给答案累加 sumsum 即可;对于两边非整块部分依然暴力求解即可。

时间复杂度

同分块 1。

代码实现

void ins(int L,int R,int c)
{
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)a[i]+=c,sum[ID1]+=c;
        return ;
    }
    for(int i=L;i<=r[ID1];i++)a[i]+=c,sum[ID1]+=c;
    for(int i=ID1+1;i<=ID2-1;i++)lazy[i]+=c,sum[i]+=c*sz;
    for(int i=l[ID2];i<=R;i++)a[i]+=c,sum[ID2]+=c;
    return ;
}

int query(int L,int R,int c)
{
    int ret=0;
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)
        {
            (ret+=(a[i]+lazy[ID1]))%=c;
        }
        return ret;
    }
    for(int i=L;i<=r[ID1];i++)(ret+=(a[i]+lazy[ID1]))%=c;
    for(int i=l[ID2];i<=R;i++)(ret+=(a[i]+lazy[ID2]))%=c;
    for(int i=ID1+1;i<=ID2-1;i++)(ret+=sum[i])%=c;
    return ret;
}

signed main()
{
    n=read();
    sz=sqrt(n);
    cnt=n/sz+((n%sz)?1:0);
    for(int i=1;i<=n;i++)
    {
        a[i]=read();
        id[i]=(i-1)/sz+1;
        sum[id[i]]+=a[i];
    }
//    sort(b+1,b+n+1);
    for(int i=1;i<=cnt;i++)
    {
        l[i]=(i-1)*sz+1;
        r[i]=i*sz;
    }
    for(int i=1;i<=n;i++)
    {
        int opt,l,r,c;
        opt=read();l=read();r=read();c=read();
        if(opt==0)ins(l,r,c);
        else cout<<query(l,r,c+1)<<'\n';
//        for(int j=1;j<=n;j++)
//        {
//            cout<<a[j]<<" "<<sum[j]<<endl;
//        }
    }
}

区间开方 区间求和

题目链接

原 loj 数列分块入门 5

思路分析

联想我们做过的P4145 上帝造题的七分钟 2 / 花神游历各国,可以发现,每次开方操作都会使一个数 xx 变成 x\sqrt x,观察数据范围:

231othersans2311−2^{31}≤others、ans≤2^{31}−1。

可以发现开方 4 次即可变成 1。

那么我们可以直接暴力开方,然后采用一种优化暴力的方法:

只要每个整块暴力开方后,记录一下元素是否都变成了 0/1,区间修改时跳过那些全为 0/1 的块即可。

显然时间复杂度可过。(懒得分析了)

代码实现

void sq(int i)
{
    if(a[i]==1||a[i]==0)return ;
    sum[id[i]]-=a[i];
    a[i]=sqrt(a[i]);
    sum[id[i]]+=a[i];
    if(a[i]==1||a[i]==0)cnt[id[i]]++;
    return ;
}

void ins(int L,int R,int c)
{
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)sq(i);
        return ;
    }
    for(int i=L;i<=r[ID1];i++)sq(i);
    for(int i=ID1+1;i<=ID2-1;i++)
    {
        if(cnt[i]==sz)continue;
        for(int j=l[i];j<=r[i];j++)sq(j);
    }
    for(int i=l[ID2];i<=R;i++)sq(i);
    return ;
}

int query(int L,int R)
{
    int ret=0;
    int ID1=id[L],ID2=id[R];
    if(ID1==ID2)
    {
        for(int i=L;i<=R;i++)ret+=a[i];
        return ret;
    }
    for(int i=L;i<=r[ID1];i++)ret+=a[i];
    for(int i=l[ID2];i<=R;i++)ret+=a[i];
    for(int i=ID1+1;i<=ID2-1;i++)ret+=sum[i];
    return ret;
}

signed main()
{
    n=read();
    sz=sqrt(n);
    CNT=n/sz+((n%sz)?1:0);
//    cnt=n/sz+((n%sz)?1:0);
    for(int i=1;i<=n;i++)
    {
        a[i]=read();
        id[i]=(i-1)/sz+1;
        sum[id[i]]+=a[i];
        if(a[i]==1||a[i]==0)cnt[id[i]]++;
    }
//    sort(b+1,b+n+1);
    for(int i=1;i<=CNT;i++)
    {
        l[i]=(i-1)*sz+1;
        r[i]=i*sz;
    }
    for(int i=1;i<=n;i++)
    {
        int opt,l,r,c;
        opt=read();l=read();r=read();c=read();
        if(opt==0)ins(l,r,c);
        else cout<<query(l,r)<<'\n';
//        for(int j=1;j<=n;j++)
//        {
//            cout<<a[j]<<" ";
//        }
//        cout<<'\n';
    }
}

单点插入 单点查询

题目链接

原 loj 数列分块入门 6

思路分析

对于单点插入操作:

可以考虑每一个块使用一个动态数组 vector,每次插入一个值,就暴力找到对应的块,然后在对应的块所在的 vector 中找到对应的插入位置并插入。

具体做法:从 1n1-n 枚举每个块,每次判断要插入的位置 xx 是否小于等于当前块的大小,若小于等于,则说明当前块就是要插入的块,然后直接插入即可;反之,则让 xx 减去当前块的大小,然后继续枚举下一个块,重复操作。

对于单点查询操作:

查询类似于插入,可以直接暴力找到对应位置然后直接输出其值即可。

但是这样又牵扯到一个问题:

如果数据不够随机,比如一个特定数据下,在一个块中一直进行插入操作,那么就会使得这个块的大小远远大于 n\sqrt n,那么每次查询的复杂度可能会很大,就会 TLE。

那么如何解决呢?

一个可行的优化办法就是,每当插入 n\sqrt n 次后,就重构数列,按照初始的分块办法,把数列较为平均的分成 n\sqrt n 个大小为 n\sqrt n 的块。

(当然实际上本题在 loj 数据偏水,所以其实并不用 rebuild 也可过)

关于语法:

如何在一个动态数组 vector 中将一个数插入到数组中特定位置?

作法:假设当前要插入的 vectorsis_i,要在 xx 之前插入值为 cc 的数,则:

s[i].insert(s[i].begin()+(x-1),c);

时间复杂度

假设每次插入的位置是 ll,那么时间复杂度约等于 O(nl)O(nl)

代码实现

void ins(int x,int c)
{
	for(int i=1;i<=cnt;i++)
	{
		if(x<=s[i].size()){s[i].insert(s[i].begin()+(x-1),c);return ;}
		x-=s[i].size();
	}
	return ;
}

void rebuild()
{
	int t=0;
	for(int i=1;i<=cnt;i++)
	{
		for(int j=0;j<s[i].size();j++)b[++t]=s[i][j];
		s[i].clear();
	}
	sz=sqrt(n);
	cnt=(n/sz)+((n%sz)?1:0);
	for(int i=1;i<=t;i++)
	{
		id[i]=(i-1)/sz+1;
		s[id[i]].push_back(b[i]);
	}
	return ;
}

int query(int x)
{
	for(int i=1;i<=cnt;i++)
	{
		if(x<=s[i].size())return s[i][x-1];
		x-=s[i].size();
	}
}
 
int main()
{
	int n;
	n=read();
	sz=sqrt(n);
	cnt=(n/sz)+((n%sz)?1:0);
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		id[i]=(i-1)/sz+1;
		s[id[i]].push_back(a[i]);
	}
	for(int i=1;i<=cnt;i++)
	{
		l[i]=(i-1)*sz+1;
		r[i]=i*sz;
	}
	int tot=0;
	for(int i=1;i<=n;i++)
	{
		int opt,l,r,c;
		opt=read();l=read();r=read();c=read();
		if(opt==0)
		{
			ins(l,r);
			tot++;
//			cout<<tot<<endl;
//			if(tot>=sz){rebuild();tot=0;}
		}
		else cout<<query(r)<<'\n';
	}
	return 0;
}

区间乘法 区间加法 单点询问

题目链接

原 loj 数列分块入门 7

思路分析

这其实就是【模板】线段树 2 - 洛谷

可以采用类似线段树的做法,使用两个懒标记,一个加法标记 lazylazy,一个乘法标记 mulmul

可以钦定乘法优先级高于加法(即:先乘后加)

对于区间加操作,每次可以直接将 lazyi+clazy_i+c 即可。

对于区间乘操作,我们每次将 lazyi+clazy_i+cmuli×cmul_i\times c 即可。

对于查询操作,我们输出 ar×mulidr+lazyidra_r \times mul_{id_r}+lazy_{id_r} 即可。

注意:

  • 对于每次更新值之前都要进行 pushdown,把当前块内的所有 aia_i 都变为原值;

  • 取模每次都要进行负数保护,即每算一次 sumsum,都要 (+mod)%mod;

时间复杂度

同分块 1。

代码实现

void pushdown(int now)
{
	for(int i=l[now];i<=r[now];i++)(a[i]=a[i]*mul[now]+lazy[now])%=mod;
	mul[now]=1;lazy[now]=0;
}
 
void ins(int L,int R,int c)
{
	int ID1=id[L],ID2=id[R];
	if(ID1==ID2)
	{
		pushdown(ID1);
		for(int i=L;i<=R;i++)(a[i]+=c)%=mod;
		return ;
	}
	pushdown(ID1);
	for(int i=L;i<=r[ID1];i++)(a[i]+=c)%=mod;
	pushdown(ID2);
	for(int i=l[ID2];i<=R;i++)(a[i]+=c)%=mod;
	for(int i=ID1+1;i<ID2;i++)(lazy[i]+=c)%=mod;
	return ;
}

void Mul(int L,int R,int c)
{
	int ID1=id[L],ID2=id[R];
	if(ID1==ID2)
	{
		pushdown(ID1);
		for(int i=L;i<=R;i++)(a[i]*=c)%=mod;
		return ;
	}
	pushdown(ID1);
	for(int i=L;i<=r[ID1];i++)(a[i]*=c)%=mod;
	pushdown(ID2);
	for(int i=l[ID2];i<=R;i++)(a[i]*=c)%=mod;
	for(int i=ID1+1;i<ID2;i++)
	{
		(mul[i]*=c)%=mod;
		(lazy[i]*=c)%=mod;
	}
	return ;
}
 
int main()
{
	int n;
	n=read();
	sz=sqrt(n);
	cnt=(n/sz)+((n%sz)?1:0);
	for(int i=1;i<=n;i++)
	{
		a[i]=read();
		id[i]=(i-1)/sz+1;
	}
	for(int i=1;i<=cnt;i++)
	{
		l[i]=(i-1)*sz+1;
		r[i]=i*sz;
		mul[i]=1;
	}
	for(int i=1;i<=n;i++)
	{
		int opt,l,r,c;
		opt=read();l=read();r=read();c=read();
		if(opt==0)ins(l,r,c%mod);
		else if(opt==1)Mul(l,r,c%mod);
		else cout<<(a[r]*mul[id[r]]+lazy[id[r]])%mod<<'\n';
	}
	return 0;
}

数列分块 1-7 完结撒花~

分块真是个很暴力的数据结构诶。

posted @ 2022-07-11 07:03  向日葵Reta  阅读(30)  评论(0编辑  收藏  举报