普通莫队
莫队算法一般分为两类,一是莫队维护区间答案,而是维护区间内的数据结构。当然也有树上莫队,带修改莫队,二维莫队等等。这篇文章主要介绍的是普通莫队算法。
我们考虑一个问题,给定一个序列,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;
}