关于简单的莫队

莫队

简介

莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。

形式

假设 \(n=m\),那么对于序列上的区间询问问题,如果从 \([l,r]\) 的答案能够 \(O(1)\) 扩展到 \([l-1,r],[l+1,r],[l,r+1],[l,r-1]\)(即与 \([l,r]\) 相邻的区间)的答案,那么可以在 \(O(n\sqrt{n})\) 的复杂度内求出所有询问的答案。

解释

离线后排序,顺序处理每个询问,暴力从上一个区间的答案转移到下一个区间答案(一步一步移动即可)。

排序方法

对于区间 \([l,r]\), 以 \(l\) 所在块的编号为第一关键字,\(r\) 为第二关键字从小到大排序。

例题

P2709 小B的询问

小B 有一个长为 \(n\) 的整数序列 \(a\),值域为 \([1,k]\)
他一共有 \(m\) 个询问,每个询问给定一个区间 \([l,r]\),求:

\[\sum\limits_{i=1}^k c_i^2 \]

其中 \(c_i\) 表示数字 \(i\)\([l,r]\) 中的出现次数。

对于 \(100\%\) 的数据,\(1\le n,m,k \le 5\times 10^4\)


先按上述的方法排序,对于每次对 \(c_i\) 的更新,贡献为 \(\pm 2c_i+1\),直接维护即可。

#include <bits/stdc++.h>
using namespace std;
#define N 50005

int n,m,k,a[N],cnt[N],ans[N];
struct node
{
    int l,r,id;
}q[N];

int cmp(node x,node y)
{
    if(x.l/250==y.l/250)
        return x.r<y.r;
    return x.l/250<y.l/250;
}

int main( void )
{
    scanf("%d%d%d",&n,&m,&k);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=m;i++)
    {
        scanf("%d%d",&q[i].l,&q[i].r);
        q[i].id=i;
    }

    sort(q+1,q+1+m,cmp);

    int l=1,r=0,now=0;
    for(int i=1;i<=m;i++)
    {
        for(int j=l;j<q[i].l;j++)
        {
            now+=cnt[a[j]]*-2+1;
            cnt[a[j]]--;
        }

        for(int j=q[i].l;j<l;j++)
        {
            now+=cnt[a[j]]*2+1;
            cnt[a[j]]++;
        }

        for(int j=q[i].r+1;j<=r;j++)
        {
            now+=cnt[a[j]]*-2+1;
            cnt[a[j]]--;
        }

        for(int j=r+1;j<=q[i].r;j++)
        {
            now+=cnt[a[j]]*2+1;
            cnt[a[j]]++;
        }

        l=q[i].l;
        r=q[i].r;
        ans[q[i].id]=now;
    }

    for(int i=1;i<=m;i++)
        printf("%d\n",ans[i]);
    
    return 0;
}

回滚莫队

简介

有些题目在区间转移时,可能会出现增加或者删除无法实现的问题。在只有增加不可实现或者只有删除不可实现的时候,就可以使用回滚莫队在 \(O(n \sqrt m)\) 的时间内解决问题。

形式

第一步还是将询问的区间按左端点所在块分组,每组内按右端点排序。
实现时对每组单独处理,以左端点所在块右端点(蓝色虚线)为界线。

  • 若左右区间在同一块内,直接暴力计算。
  • 在界线右边的区间(灰色线)的右端点单调不降,可以顺着遍历每个位置。
  • 在界限左边的区间(绿色线)的左端点无序,每次都需要重新遍历,答案在右边的基础上贡献。

复杂度证明

设分块大小为 \(b\):

  • 对于左右区间在同一块内的询问,每次计算复杂度 \(O(b)\)
  • 对于界线左边的区间,每次只会在左端点所在块内计算,复杂度 \(O(b)\)
  • 对于界限右边的区间,在每一组内顺着遍历,最多 \(n\) 次,而有 \(\frac{n}{d}\) 组。

总复杂度为 \(O(mb+\frac{n^2}{d})\)\(b=\frac{n}{\sqrt{m}}\) 时最优,为 \(O(n\sqrt{m})\)

例题

不删除莫队

P5906 【模板】回滚莫队&不删除莫队

给定一个序列,多次询问一段区间 \([l,r]\),求区间中相同的数的最远间隔距离
序列中两个元素的间隔距离指的是两个元素下标差的绝对值

对于 \(40\%\) 的数据,满足 \(1\leq a_i \leq 400\)\(1\leq n,m\leq 60000\)
对于 \(100\%\) 的数据,满足 \(1\leq n,m\leq 2\cdot 10^5\)\(1\leq a_i\leq 2\cdot 10^9\)


按上述方法实现就行,详细请看代码注释。

#include <bits/stdc++.h>
using namespace std;
#define N 200005

int n,m,q,a[N],tot,top,ans[N];

//一个块的左端点、右端点和某点所在块
int l[505],r[505],to[N];
//界线右边区间对应数的最小、最大下标 和界线左边区间对应数的最小、最大下标
int l1[N],r1[N],l2[N],r2[N];
//当前界限右边的答案和总区间的答案
int now;

pair<int,int> p[N];
struct node
{
    int l,r,id;
}st[N];
vector<node>b[505];

//求l、r和to
void init()
{
    m=sqrt(n);
    
    for(int i=1;i<=m;i++)
    {
        l[i]=r[i-1]+1;
        r[i]=l[i]+m-1;
    }
    
    if(r[m]<n)
    {
        m++;
        l[m]=r[m-1]+1;
        r[m]=n;
    }
    
    for(int i=1;i<=m;i++)
        for(int j=l[i];j<=r[i];j++)
            to[j]=i;
}

bool cmp(node i,node j)
{
    return i.r<j.r;
}

int main( void )
{
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        p[i]={a[i],i};
    }
    
    //离散化
    sort(p+1,p+1+n);
    for(int i=1;i<=n;i++)
    {
        if(p[i].first!=p[i-1].first)
            tot++;
        a[p[i].second]=tot;
    }
        
    init();
    
    scanf("%d",&q);
    for(int i=1;i<=q;i++)
    {
        int x,y;
        scanf("%d%d",&x,&y);
        b[to[x]].push_back({x,y,i});
    }
    
    for(int i=1;i<=m;i++)
        sort(b[i].begin(),b[i].end(),cmp);
    
    //如果像普通莫队那样直接遍历整个q每次都需要判断是否在新的组内,比较麻烦
    //可为每组开一个vector,对其分开计算
    for(int i=1;i<=m;i++)
    {
        int len=b[i].size();
        if(!len)	
            continue;
        
        //这里的l1得赋较大值,否则后面求min(l1,l2)时会算错
        memset(l1,0x3f,sizeof(l1));
        memset(r1,0,sizeof(r1));
        int x=r[to[b[i][0].l]]+1,y=x-1;
        now=0;
        
        for(int j=0;j<len;j++)
        {
            //在同一块内暴力算
            if(to[b[i][j].l]==to[b[i][j].r])
            {
                for(int k=b[i][j].l;k<=b[i][j].r;k++)
                {
                    if(!l2[a[k]])
                        l2[a[k]]=k;
                    r2[a[k]]=k;
                    ans[b[i][j].id]=max(ans[b[i][j].id],r2[a[k]]-l2[a[k]]);
                }
                
                //复原时不能memset
                for(int k=b[i][j].l;k<=b[i][j].r;k++)
                    l2[a[k]]=r2[a[k]]=0;
                
                continue;
            }
            
            //算界线右边的
            for(int k=y+1;k<=b[i][j].r;k++)
            {
                if(l1[a[k]]==l1[0])
                    l1[a[k]]=k;
                r1[a[k]]=k;
                now=max(now,r1[a[k]]-l1[a[k]]);
            }
            
            ans[b[i][j].id]=now;
            top=0;
            
            //算界线左边的
            for(int k=x-1;k>=b[i][j].l;k--)
            {
                l2[a[k]]=k;
                if(!r2[a[k]])
                    r2[a[k]]=k;
                ans[b[i][j].id]=max(ans[b[i][j].id],max(r1[a[k]],r2[a[k]])-min(l1[a[k]],l2[a[k]]));
            }
            
            //复原
            for(int k=x-1;k>=b[i][j].l;k--)
                l2[a[k]]=r2[a[k]]=0;
            
            //右指针向右跳
            y=b[i][j].r;
        }
    }
    
    for(int i=1;i<=q;i++)
        printf("%d\n",ans[i]);
    
    return 0;
}

不增加莫队

P4137 Rmq Problem / mex

有一个长度为 \(n\) 的数组 \(\{a_1,a_2,\ldots,a_n\}\)
\(m\) 次询问,每次询问一个区间内最小没有出现过的自然数。

对于 \(30\%\) 的数据:\(1\leq n,m\leq 1000\)
对于 \(100\%\) 的数据:\(1\leq n,m\leq 2\times {10}^5\)\(1\leq l\leq r\leq n\)\(0\leq a_i\leq 2\times 10^5\)

一个集合内最小没有出现过的自然数通常被称为mex。


如果连续插入数,难以维护mex。

容易发现,如果已知mex与一个集合,每次删除其中的数,则mex的变化只会有两种可能:

  • 若当前要删的数 \(x\) 只剩一个,并且 \(x<mex\),则删后 \(mex\) 变为 \(x\);
  • 否则,\(mex\) 不变。

因此也可以用回滚莫队实现,对于每一组内按右端点降序排序,先求出其左侧所在块的左端点到最大右端点的mex,再考虑每次删去一个数时能否更新的mex。

#include <bits/stdc++.h>
using namespace std;

int n,m,q,a[200005],l[505],r[505],to[200005],flag1[200005],flag2[200005],now,ans[200005];
struct node
{
    int l,r,id;
}b[200005];

void init()
{
    m=sqrt(n);
    
    for(int i=1;i<=m;i++)
    {
        l[i]=r[i-1]+1;
        r[i]=l[i]+m-1;
    }
    
    if(r[m]<n)
    {
        m++;
        l[m]=r[m-1]+1;
        r[m]=n;
    }
    
    for(int i=1;i<=m;i++)
        for(int j=l[i];j<=r[i];j++)
            to[j]=i;
}

bool cmp(node i,node j)
{
    if(to[i.l]==to[j.l])
        return i.r>j.r;
    return to[i.l]<to[j.l];
}

int main( void )
{
    scanf("%d%d",&n,&q);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    for(int i=1;i<=q;i++)
    {
        scanf("%d%d",&b[i].l,&b[i].r);
        b[i].id=i;
    }
    
    init();
    
    sort(b+1,b+1+q,cmp);
    int x=0,y=0;
    
    //这里也可以用上一题的写法,懒得改了QwQ
    for(int i=1;i<=q;i++)
    {
        //在同一块内暴力算
        if(to[b[i].l]==to[b[i].r])
        {
            for(int j=b[i].l;j<=b[i].r;j++)
                flag2[a[j]]=1;
            
            ans[b[i].id]=n;
            if(!flag2[0])
                ans[b[i].id]=0;
                
            for(int j=b[i].l;j<=b[i].r;j++)
                if(!flag2[a[j]+1])
                    ans[b[i].id]=min(ans[b[i].id],a[j]+1);
            

            //复原
            for(int j=b[i].l;j<=b[i].r;j++)
                flag2[a[j]]=0;
            
            continue;
        }
        
        //如果这次询问和上次不在一组
        if(to[b[i].l]!=to[x])
        {
            for(int j=l[to[x]];j<=y;j++)
                flag1[a[j]]=0;
            now=n;
            
            for(int j=l[to[b[i].l]];j<=b[i].r;j++)
                flag1[a[j]]++;
            
            //暴力算mex
            for(int j=0;j<n;j++)
                if(!flag1[j])
                {
                    now=j;
                    break;
                }
            
            x=b[i].l;
            y=b[i].r;
        }
        
        //减右边的
        for(int j=y;j>b[i].r;j--)
        {
            flag1[a[j]]--;
            if(!flag1[a[j]])
                now=min(now,a[j]);
        }
        
        ans[b[i].id]=now;
        
        //减左边的
        for(int j=l[to[b[i].l]];j<b[i].l;j++)
        {
            flag1[a[j]]--;
            if(!flag1[a[j]])
                ans[b[i].id]=min(ans[b[i].id],a[j]);
        }
        
        //复原
        for(int j=l[to[b[i].l]];j<b[i].l;j++)
            flag1[a[j]]++;
            
        x=b[i].l;
        y=b[i].r;
    }
    
    for(int i=1;i<=q;i++)
        printf("%d\n",ans[i]);
    
    return 0;
}
posted @ 2025-04-22 15:47  ShanLing3  阅读(56)  评论(0)    收藏  举报
//雪花飘落效果