莫队算法初探

普通莫队

莫队算法一般分为两类,一是莫队维护区间答案,而是维护区间内的数据结构。当然也有树上莫队,带修改莫队,二维莫队等等。这篇文章主要介绍的是普通莫队算法。

我们考虑一个问题,给定一个序列,m次询问,每次询问区间[l,r]有多少种不同的颜色。n,m<100000

先考虑暴利,对每次询问遍历一遍[l,r],这样是O(nm)的。
换种方式暴利,定义ql和qr,表示区间[ql,qr]内有多少种颜色,再定义cnt数组,cnti表示第i种颜色在区间[ql,qr]中出现了多少次。

我们一个一个询问处理,对于询问[l,r],我们挪动ql到l,qr到r
因为这个是莫队算法的基础,所以模拟一下整个过程:
在这里插入图片描述
我们的初始在这个状态,假设蓝色为1,红色为2,绿色为3
那么cnt1=3 , cnt2=3 , cnt3=1,我们把qr向右挪一下
在这里插入图片描述
这样就多了一个绿色,cnt3加1
继续挪一下
在这里插入图片描述
多了一个红色,cnt2加上1
此时我们发现,我们的右指针已经和询问的右端点重合了,接下来挪左指针
在这里插入图片描述
少了一个蓝色,cnt1减去1
现在我们得出的答案为3,cnt1=2 , cnt2=4 , cnt3=2
提供这部分的代码:

inline void add(int x)
{
    cnt[x]++;
    if(cnt[x]==1) ans++;
}
inline void del(int x)
{
    cnt[x]--;
    if(cnt[x]==0) ans--;
}
while(l>q[i].l) add(a[--l]);
while(r<q[i].r) add(a[++r]);
while(l<q[i].l) del(a[l++]);
while(r>q[i].r) del(a[r--]);

我们发现,每次挪动都是O(1)的,每次询问我们最多还是要挪动n次,所以时间复杂度还是O(nm)。

我们有没有办法来加速呢?

这种暴力的耗时就耗在挪动次数上,我们要让他挪动的次数尽量少。肯定是有的,我们先将询问存储下来,再按照某种方法排序,让他减少挪动的次数,这样会快一些。

这种方法是强行离线,然后排序,所以这样的普通莫队不支持修改。

那么怎么排序呢?

一种解决方式是优先按照左端点排序。
这种排序方式,保证左端点只会向右挪,但是右端点每次最坏还是可以从最前面挪到最后面,从最后面移到最前面,这样的复杂度还是O(nm),是不行的。

我们的排序是要使左右端点挪动的次数尽量少,所以这里就有一种排序方法:将序列分为n1/2个长度为n1/2的块,若左端点在同一个块内,就按右端点排序(以左端点为第一关键字,右端点为第二关键字)

注意,莫队的挪动操作要尽量块,若不是O(1)的,复杂度会原地爆炸,对于n与m同阶,一般可以设长度为n1/2,复杂度为n*n1/2。(想看证明的话可以点击这里

但是对于m的其他取值,如m>n,分块方式需要改变才能变得更优。怎么分块呢?
在这里插入图片描述
普通莫队的优化
我们看一下下面这组数据

// 设块的大小为 2 (假设)
1 1
2 100
3 1
4 100

手动模拟一下可以发现,r指针的移动次数大概为300次,我们处理完第一个块之后,l=2,r=100,此时只需要移动两次l指针就可以得到第四个询问的答案,但是我们却将r指针移到起来获取第三个询问的答案,再移动100获取第四个询问的答案,这样就多了九十几次的指针移动。我们怎么优化这个地方呢?这里我们就要用到奇偶化排序了。

什么是奇偶化排序?奇偶化排序即对于奇数块的询问,r从小到大排序,对于偶数块的询问,r从大到小排序,这样我们的r指针在处理完奇数块的问题后,将在返回的途中处理偶数块的问题,再将n移动处理下一个奇数块的问题,优化了r指针的移动次数,一半情况下,这种优化能让程序块30%

排序代码:

struct node
{
    int l,r,id;
    bool operator<(const node &x) const
    {
        if(l/unit!=x.l/unit) return l<x.l;
        if((l/unit)&1) return r<x.r;//注意这里和下面一行不能写小于(大于)等于,否则会出错
        return r>x.r;
    }
}

例题:P1972 [SDOI2009]HH的项链

现在因为数据加强了,好像莫队已经过不了了,但是用来熟悉莫队的基本操作还是可以的。

完整代码

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int a[N],unit;
struct node
{
    int l,r,id;
    bool operator<(const node &x) const 
    {
        if(l/unit!=x.l/unit) return l<x.l;
        if((l/unit)&1) return r<x.r;
        return r>x.r;
    }
}q[N];
int res[N],cnt[N],ans;
void add(int x)
{
    if(!cnt[a[x]]) ans++;
    cnt[a[x]]++;
}
void del(int x)
{
    cnt[a[x]]--;
    if(!cnt[a[x]]) ans--;
}
int main()
{
    int n;
    scanf("%d",&n);
    for(int i=1;i<=n;i++)
        scanf("%d",&a[i]);
    int m;
    scanf("%d",&m);
    unit=n/sqrt(m);
    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);
    int l=0,r=0;
    for(int i=1;i<=m;i++)
    {
        int ql=q[i].l,qr=q[i].r;
        while(l<ql) del(l++);
        while(l>ql) add(--l);
        while(r<qr) add(++r);
        while(r>qr) del(r--);
        res[q[i].id]=ans;
    }
    for(int i=1;i<=m;i++)
        printf("%d\n",res[i]);
    //system("pause");
    return 0;
}
  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值