分块

分块

前言

在了解过树状数组和线段数之后,我们已经能处理许多区间的信息修改和查询的题目。但当信息不具有区间可加性时,用树状数组和线段树就不好处理了,这时候就可以用到一种优雅的暴力——分块。

简介

分块是一种思想,通过适当的划分,预处理一部分信息并保留,用空间换时间达到时空平衡,来完成对数列一些区间操作和区间查询的操作。效率比不上线段树和树状数组,大概是根号级别的,但它更加通用,容易实现,可以维护更复杂的信息。

具体操作

建块

首先要明确我们需要什么:

  1. 块的大小
  2. 块的数量
  3. 块的左右边界
  4. 每个元素所属的块

首先是块的大小,块的大小具体要依据实际,使时间复杂度最小。一般情况下大小为 \(\sqrt{n}\)

sq=sqrt(n);

接下来是块的数量,要注意 \(n\) 不一定是个完全平方数,最后剩下的一些元素组成一个组。

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

确定块的左右边界,直接找规律。\(L_1=1,R_1=sq,L_2=sq+1,R_2=2 \times sq , \cdots\)

得到规律:

\[L_i=(x-1) \times sq+1,R_x=x \times sq \]

还要注意 \(R_{tot}=n\)

for(int i=1;i<=tot;i++)
{
	L[i]=(i-1)*block+1;
    R[i]=i*block;
}
R[tot]=n;

确定每个元素所属的块,还是找规律。

for(int i=1;i<=n;i++) b[i]=(i-1)/sq+1;

总体代码:

sq=sqrt(n);
tot=n/sq;
if(n%sq) tot++;
for(int i=1;i<=n;i++)
{
    cin>>a[i];
    b[i]=(i-1)/sq+1;
}
for(int i=1;i<=tot;i++)
{
    L[i]=(i-1)*sq+1;
    R[i]=i*sq;
}
R[tot]=n;

修改

以维护区间和为例。

修改区间为 \([l,r]\),这个区间覆盖的不一定都是整块,会出现只有块中几个元素的碎块,如图:
img
共有左碎块,整块,右碎块三个部分,分别处理。

\(x\),\(y\) 为左碎块和右碎块的块编号。

在整块中,不必具体维护每个元素,使用和线段树类似的懒标记,将修改的信息存在里面,又类似于标记永久化,不必去修改原数组中的元素,在使用的时候加上就可以了。也就是说\(a_i\) 实际上是 \(a_i+lazy_i\)。由图可知,整块的块编号为 \([x+1,y-1]\)

对于碎块,碎块的元素个数最多不超过两倍的块的大小,可以直接暴力修改。由图可知,左碎块的区间为 \([l,R_x]\),右碎块的区间为 \([L_y,r]\)

需要特判一下 \(x=y\) 的情况。

void update(int l,int r,int k)
{
    int x=b[l],y=b[r];
    if(x==y)
    {
        for(int i=l;i<=r;i++) a[i]+=k,v[x]+=k;//v为区间和
    }
    else 
    {
        for(int i=l;i<=R[x];i++) a[i]+=k,v[x]+=k;//左碎块
        for(int i=L[y];i<=r;i++) a[i]+=k,v[y]+=k;//右碎块
        for(int i=x+1;i<=y-1;i++) lazy[i]+=k,v[i]+=k*sq;//整块
    }
}

查询

同样以维护区间和为例。

与修改类似,对于整块,直接加和。对于碎块,暴力加答案。

int query(int l,int r,int k)
{
    int ans=0;
    int x=b[l],y=b[r];
    if(x==y)
    {
        for(int i=l;i<=r;i++) ans+=a[i]+lazy[x];
    }
    else 
    {
        for(int i=l;i<=R[x];i++) ans+=a[i]+lazy[x];
        for(int i=L[y];i<=r;i++) ans+=a[i]+lazy[y];
        for(int i=x+1;i<=y-1;i++) ans+=v[i];
    }
    return ans;
}

接下来就可以来做一些简单的例题了。

P2357 守墓人

https://www.luogu.com.cn/problem/P2357

分析

简单的板子题,将上述的一些操作结合就好,维护区间和。

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=2e5+5;

ll n,m,sq,tot,a[N],b[N],v[N],L[N],R[N],lazy[N];
void add(int l,int r,int k)
{
    int x=b[l],y=b[r];
    if(x==y)
    {
        for(int i=l;i<=r;i++) a[i]+=k,v[x]+=k;//v为区间和
    }
    else 
    {
        for(int i=l;i<=R[x];i++) a[i]+=k,v[x]+=k;//左碎块
        for(int i=L[y];i<=r;i++) a[i]+=k,v[y]+=k;//右碎块
        for(int i=x+1;i<=y-1;i++) lazy[i]+=k,v[i]+=k*sq;//整块
    }
}
ll query(int l,int r)
{
    ll ans=0;
    ll x=b[l],y=b[r];
    if(x==y)
    {
        for(int i=l;i<=r;i++) ans+=a[i]+lazy[x];
    }
    else 
    {
        for(int i=l;i<=R[x];i++) ans+=a[i]+lazy[x];
        for(int i=L[y];i<=r;i++) ans+=a[i]+lazy[y];
        for(int i=x+1;i<=y-1;i++) ans+=v[i];
    }
    return ans;
}
int main ()
{
    ios::sync_with_stdio(false);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    sq=sqrt(n);
    tot=n/sq;
    if(n%sq) tot++;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        b[i]=(i-1)/sq+1;
        v[b[i]]+=a[i];
    }
    for(int i=1;i<=tot;i++)
    {
        L[i]=(i-1)*sq+1;
        R[i]=i*sq;
    }
    R[tot]=n;
    ll op,l,r,k;
    while(m--)
    {
        cin>>op;
        if(op==1) 
        {
            cin>>l>>r>>k;
            add(l,r,k);
        }
        else if(op==2)
        {
            cin>>k;
            add(1,1,k);
        }
        else if(op==3) 
        {
            cin>>k;
            add(1,1,-k);
        }
        else if(op==4)
        {
            cin>>l>>r;
            cout<<query(l,r)<<"\n";
        }
        else cout<<query(1,1)<<"\n";
    }
    return 0;
}

P3870 [TJOI2009] 开关

https://www.luogu.com.cn/problem/P3870

分析

也是类似于板子题,不过操作变成了区间取反。

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e5+5;

int a[N],b[N],lazy[N],v[N],m,n,sq;
void change(int i)
{
    v[b[i]]-=a[i]^lazy[b[i]],a[i]^=1,v[b[i]]+=a[i]^lazy[b[i]];
}
void update(int l,int r)
{
    int x=b[l],y=b[r];
    if(x==y) for(int i=l;i<=r;i++) change(i);
    else 
    {
        for(int i=l;i<=x*sq;i++) change(i);
        for(int i=(y-1)*sq+1;i<=r;i++) change(i);
        for(int i=x+1;i<=y-1;i++) lazy[i]^=1,v[i]=sq-v[i];
    }

}
int query(int l,int r)
{
    int x=b[l],y=b[r],ans=0;
    if(x==y) for(int i=l;i<=r;i++) ans+=a[i]^lazy[b[i]];
    else 
    {
        for(int i=l;i<=x*sq;i++) ans+=a[i]^lazy[b[i]];
        for(int i=(y-1)*sq+1;i<=r;i++) ans+=a[i]^lazy[b[i]];
        for(int i=x+1;i<=y-1;i++) ans+=v[i];
    }
    return ans;
}
int main ()
{
    cin>>n>>m;
    sq=sqrt(n);
    for(int i=1;i<=n;i++) b[i]=(i-1)/sq+1;
    while(m--)
    {
        int op,x,y;
        cin>>op>>x>>y;
        if(op==0) update(x,y);
        else cout<<query(x,y)<<"\n";
    }
    return 0;
}

P2801 教主的魔法

https://www.luogu.com.cn/problem/P2801

分析

求区间中大于等于 \(c\) 的个数。可以将每个块分别排序。

对于整块来说,因为已经排好了序,所以可以二分查找。对于碎块来说,也是暴力统计。注意修改时要重新排序碎块,整块不用排序。

时间复杂度为 \(O(n \log{n})\)

code

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
const int N=1e6+5;

int n,m,tot,sq,R[N],L[N],lazy[N],a[N],b[N],c[N];
void update(int l,int r,int k)
{
    int x=b[l],y=b[r];
    if(x==y)
    {
        for(int i=l;i<=r;i++) a[i]+=k;
        for(int i=L[x];i<=R[y];i++) c[i]=a[i];
        sort(c+L[x],c+R[y]+1);
    }
    else 
    {
        for(int i=l;i<=R[x];i++) a[i]+=k;
        for(int i=L[x];i<=R[x];i++) c[i]=a[i];
        sort(c+L[x],c+R[x]+1);
        for(int i=L[y];i<=r;i++) a[i]+=k;
        for(int i=L[y];i<=R[y];i++) c[i]=a[i];
        sort(c+L[y],c+R[y]+1);
        for(int i=x+1;i<=y-1;i++) lazy[i]+=k;
    }
}
int query(int l,int r,int k)
{
    int ans=0,x=b[l],y=b[r];
    if(x==y) 
    {
        //这里不能写成一行QAQ,不然else会与后面的if相关
        for(int i=l;i<=r;i++) 
            if(lazy[x]+a[i]>=k) 
                ans++;
    }
    else 
    {
        for(int i=l;i<=R[x];i++) if(lazy[x]+a[i]>=k) ans++;
        for(int i=L[y];i<=r;i++) if(lazy[y]+a[i]>=k) ans++;
        for(int i=x+1;i<=y-1;i++) 
        {
            int ll=L[i],rr=R[i],cnt=0,mid;
            while(ll<=rr)
            {
                mid=(ll+rr)>>1;
                if(c[mid]+lazy[i]>=k) rr=mid-1,cnt=R[i]-mid+1;
                else ll=mid+1;
            }
            ans+=cnt;
        }
    }
    return ans;
}
int main ()
{
    ios::sync_with_stdio(0);
    cin.tie(0);cout.tie(0);
    cin>>n>>m;
    sq=sqrt(n);
    tot=n/sq;
    if(n%sq) tot++;
    for(int i=1;i<=n;i++)
    {
        cin>>a[i];
        b[i]=(i-1)/sq+1;
        c[i]=a[i];
    }
    for(int i=1;i<=tot;i++)
    {
        L[i]=(i-1)*sq+1;
        R[i]=i*sq;
    }
    R[tot]=n;
    for(int i=1;i<=tot;i++) sort(c+L[i],c+R[i]+1);
    char op;
    int l,r,k;
    while(m--)
    {
        cin>>op>>l>>r>>k;
        if(op=='M') update(l,r,k);
        else cout<<query(l,r,k)<<"\n";
    }
    return 0;
}

tip

考虑分块时,要先想整块如何去维护,碎块一般都是暴力维护。

posted @ 2024-02-23 16:26  zhouruoheng  阅读(15)  评论(0编辑  收藏  举报