其他分享
首页 > 其他分享> > 分块/莫队总结(未完结)

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

作者:互联网

分块/莫队总结

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

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

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

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

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


基本思想

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

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

基本操作

区间加法 单点查询

题目链接

原 loj 数列分块入门 1

思路分析

将原数列划分为大约 \(\sqrt n\) 个长度至多为 \(\sqrt n\) 的若干个不同的块。预处理出来原数列中的每个位置 \(x\) 所属块 \(id_x\),以及每一个块 \(i\) 的左右端点 \(l_i\) 和 \(r_i\)。

对于区间加法:

假设执行该操作的区间是 \([L,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]\)。

那么对于两边的两个部分可以直接暴力求解,直接对于每一个 \(i\) 使得 \(a_i+c\)。

对于中间的若干块,可以用一个类似于线段树的 lazy 标记记录一下要加的值,即对于每一个块 \(i\) 使得 \(lazy_i+c\)。

对于单点查询:直接输出 \(a_r+lazy_{id_r}\) 即可。

时间复杂度

\(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;
}

区间加法 区间计数(小于 \(x\))

题目链接

原 loj 数列分块入门 2

思路分析

区间加法同上。

对于找区间内小于 \(x\) 的个数:

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

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

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

我们来分析一下。

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

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

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

易错点

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

时间复杂度

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

总复杂度:\(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';
    }
}

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

区间加法 区间前驱查询(查 \(x\) 的前驱)

题目链接

原 loj 数列分块入门 3

思路分析

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

答:比其小的最大元素。

区间加法同上。

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

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

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

另外在查询操作时应该初始化答案为某一个到达不了的极小值,最好不要用 -1。(虽然本题 -1 可以过,但原数据范围的大小是 \(-2^{31}\) 到 \(2^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

思路分析

区间加法同上。

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

时间复杂度

同分块 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 / 花神游历各国,可以发现,每次开方操作都会使一个数 \(x\) 变成 \(\sqrt x\),观察数据范围:

\(−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

思路分析

标签:未完结,分块,int,整块,重载,区间,莫队,id
来源: https://www.cnblogs.com/xrkforces/p/partitioning.html