莫队算法
莫队,莫涛,长郡中学学生,算我半个学长了,后进入清华大学学习,IOI中国国家队队长发明了“莫队算法”,在国外名字叫做"Mo's Algorithm"。
莫队算法的精髓就是通过合理地对询问排序,然后以较优的顺序暴力回答每个询问。处理完一个询问后,可以使用它的信息得到下一个询问区间的答案。
我们设置一个结构体,存储我们需要的变量,有l,r,id,可能还有其他的。
struct Mo{int l,r,ID,k;int ans;}q[N];
那这个较优的顺序暴力是如何做到的呢?这里运用分块的思想,如果是属于同一块的的话,那么比较左区间;如果不是同一块的话就比较右区间。这样保证我的左右指针在每一次与上一次询问移动的不多。如果不排序,像是[1,2]和[99999,100000]这样的数据那会花费大量的时间移动指针。(代码如下)
bool cmp(Mo a,Mo b){return belong[a.l]==belong[b.l]?a.r<b.r:a.l<b.l;}
对于处理下一个询问,莫队喜欢用while循环移动,判断l,r两个指针的移动方向,直到指针与区间相等。(代码如下)
l=1;r=0;
for (int i=1;i<=m;i++)
{
while(l<q[i].l) del(l++);
while(l>q[i].l) add(--l);
while(r<q[i].r) add(++r);
while(r>q[i].r) del(r--);
q[i].ans=...
}
然后每一次处理对于移动前要减去,然后处理,最后将处理完的加上来,这样保证答案对于当前的l,r是正确的,那么到最后的区间也是正确的。(代码如下)
void add(int x)
{
num[sum[a[x]]]--;
sum[a[x]]++;
num[sum[a[x]]]++;
}
void del(int x)
{
num[sum[a[x]]]--;
sum[a[x]]--;
num[sum[a[x]]]++;
}
//num和sum举个例子
我们在存入q的时候,将询问的顺序也存入进去,最后按从1..n的顺序排列输出,就是我们要的答案。或者在for循环里面每一次存ans[ q[i].ID ]=...,这样不用排序也可以。(代码如下)
bool CMP(Mo a,Mo b){return a.ID<b.ID;};
题目1
J-附加题Ⅱ_【第一届】无锡太湖学院ICPC校队对抗赛 (nowcoder.com)
给出一个总数为的序列,有次询问,求内数量为的有多少个。
思路
大致思路如上,我们每一次移动,若增加(add)就将当前的a[x]的数量的num数组-1,然后a[x]++,因为a[x]的数量变了,之前那个数量没有更新,现在这个数量更新了,所以a[x]的数量更新进去,num数组+1.
代码
#include<stack>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<queue>
#include<cstring>
#include<deque>
#include<vector>
#include<iostream>
#include<map>
#include<set>
#include<iomanip>
#define go(i,a,b) for(int i=a;i<=b;i++)
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define ll long long
using namespace std;
const int inf=0x3f3f3f3f;
const int N = 4e4+7;
struct Mo{int l,r,ID,k;int ans;}q[N];
ll GCD(ll a,ll b){while(b^=a^=b^=a%=b);return a;}
int n,m,block,belong[N];
int sum[N],ans,num[N],a[N];
bool cmp(Mo a,Mo b){return belong[a.l]==belong[b.l]?a.r<b.r:a.l<b.l;}
bool CMP(Mo a,Mo b){return a.ID<b.ID;};
void add(int x)
{
num[sum[a[x]]]--;
sum[a[x]]++;
num[sum[a[x]]]++;
}
void del(int x)
{
num[sum[a[x]]]--;
sum[a[x]]--;
num[sum[a[x]]]++;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("IO\\in.txt","r",stdin);
freopen("IO\\out.txt","w",stdout);
#endif
IOS
scanf("%d%d",&n,&m);block=sqrt(n);
for (int i=1;i<=n;i++)
scanf("%d",&a[i]),belong[i]=(i-1)/block+1;;
for (int i=1;i<=m;i++)
scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].k),q[i].ID=i;
sort(q+1,q+m+1,cmp);
int l=1,r=0;
for (int i=1;i<=m;i++)
{
while(l<q[i].l) del(l++);
while(l>q[i].l) add(--l);
while(r<q[i].r) add(++r);
while(r>q[i].r) del(r--);
q[i].ans=num[ q[i].k ];
}
sort(q+1,q+m+1,CMP);
for (int i=1;i<=m;i++)
cout<<q[i].ans<<endl;
return 0;
}
题目2 - SP3267 DQUERY - D-query
SP3267 DQUERY - D-query - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
不过这个题好像在洛谷里面上交不了,见vjudge链接
D-query - SPOJ DQUERY - Virtual Judge (vjudge.net)
给出一个总数为的序列,有次询问,求内不同种类的数有多少个。
思路
大致的框架是一样的,分块之后sort,然后for循环1..m,里面有四个while循环,有增加和删除,最后sort回来输出。这个数据不是很强,分块不优化可以过。
判断删除增加是否对答案产生影响,一个数组可以来统计,最后now变量就是我们要的答案。
void add(int x)
{
if (!cnt[a[x]]) now++;
cnt[a[x]]++;
}
void del(int x)
{
cnt[a[x]]--;
if (!cnt[a[x]]) now--;
}
代码
#include<stack>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<queue>
#include<cstring>
#include<deque>
#include<vector>
#include<iostream>
#include<map>
#include<set>
#include<iomanip>
#define go(i,a,b) for(int i=a;i<=b;i++)
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define ll long long
using namespace std;
const int inf=0x3f3f3f3f;
const int N = 2e6+7;
struct Mo{int l,r,ID,k;int ans;}q[N];
ll GCD(ll a,ll b){while(b^=a^=b^=a%=b);return a;}
int n,m,block,belong[N];
int sum[N],ans,num[N],a[N];
bool cmp(Mo a,Mo b){return belong[a.l]==belong[b.l]?a.r<b.r:a.l<b.l;}
bool CMP(Mo a,Mo b){return a.ID<b.ID;};
int mp[N],cnt[N],now;
int exist[N];
void add(int x)
{
if (!cnt[a[x]]) now++;
cnt[a[x]]++;
}
void del(int x)
{
cnt[a[x]]--;
if (!cnt[a[x]]) now--;
}
signed main()
{
#ifndef ONLINE_JUDGE
freopen("IO\\in.txt","r",stdin);
freopen("IO\\out.txt","w",stdout);
#endif
IOS
cin>>n;block=sqrt(n);
for (int i=1;i<=n;i++)
cin>>a[i],belong[i]=(i-1)/block+1;;
cin>>m;
for (int i=1;i<=m;i++)
cin>>q[i].l>>q[i].r,q[i].ID=i;
sort(q+1,q+m+1,cmp);
int l=1,r=0;
//cout<<m<<endl;
//for (int i=1;i<=m;i++) cout<<q[i].l<<" "<<q[i].r<<endl;
for (int i=1;i<=m;i++)
{
//cout<<i<<endl;
while(l<q[i].l) del(l++);
while(l>q[i].l) add(--l);
while(r<q[i].r) add(++r);
while(r>q[i].r) del(r--);
q[i].ans=now;
}
sort(q+1,q+m+1,CMP);
for (int i=1;i<=m;i++)
cout<<q[i].ans<<endl;
return 0;
}
题目3 - P1972 [SDOI2009]HH的项链
P1972 [SDOI2009]HH的项链 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)
思路
这个题跟上个题一模一样,是数据加强版,正好来体验一下莫队的优化,最后wa了26发才AC,而且那个点还是卡着1.5s的限制。让我们来看看怎么样优化吧。
1.对于莫队的优化
1)玄学的奇偶排序
将原本的cmp
bool cmp(Mo a,Mo b){return belong[a.l]==belong[b.l]?a.r<b.r:a.l<b.l;}
换成奇偶排序,并用位运算
int cmp(query a, query b) {
return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
也就是说,对于左端点在同一奇数块的区间,右端点按升序排列,反之降序。这个东西也是看着没用,但实际效果显著。
它的主要原理便是右指针跳完奇数块往回跳时在同一个方向能顺路把偶数块跳完,然后跳完这个偶数块又能顺带把下一个奇数块跳完。理论上主算法运行时间减半,实际情况有所偏差。
2)while循环不调用参数
将原本的
void add(int x)
{
if (!cnt[a[x]]) now++;
cnt[a[x]]++;
}
void del(int x)
{
cnt[a[x]]--;
if (!cnt[a[x]]) now--;
}
for (int i=1;i<=m;i++)
{
while(l<q[i].l) del(l++);
while(l>q[i].l) add(--l);
while(r<q[i].r) add(++r);
while(r>q[i].r) del(r--);
q[i].ans=now;
}
换成不调用参数的,常数压缩
for (register int i=1;i<=m;i++)
{
while(l<q[i].l) now -= !--cnt[a[l++]];
while(l>q[i].l) now += !cnt[a[--l]]++;
while(r<q[i].r) now += !cnt[a[++r]]++;
while(r>q[i].r) now -= !--cnt[a[r--]];
//q[i].ans=now;
answer[q[i].ID]=now;
//最后不进行sort 直接按照answer输出
}
3)最后不sort,存答案直接1..m输出
同上的注释
4)分块的长度block优化
在分块九讲里面,有一题需要手动设置block,因为数据太大了。这个数据加强版我设置了好几个值,最后block在2300(不唯一)才过,有些小的和大的都没过...
2.基础优化
1)快读快输,加上inline
inline int read()
{
register int X=0; bool flag=1; char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+ch-'0'; ch=getchar();}
if(flag) return X;
return ~(X-1);
}
inline void write(int X)
{
if(X<0) {X=~(X-1); putchar('-');}
if(X>9) write(X/10);
putchar(X%10+'0');
}
2)register用法
for (register i=1;i<=m;i++)
3)O2优化
代码
#include<stack>
#include<cstdlib>
#include<cstdio>
#include<algorithm>
#include<cmath>
#include<queue>
#include<cstring>
#include<deque>
#include<vector>
#include<iostream>
#include<map>
#include<set>
#include<iomanip>
#define go(i,a,b) for(int i=a;i<=b;i++)
#define mem(a,b) memset(a,b,sizeof(a))
#define IOS ios::sync_with_stdio(false);cin.tie(0);cout.tie(0);
#define ll long long
using namespace std;
inline int read()
{
register int X=0; bool flag=1; char ch=getchar();
while(ch<'0'||ch>'9') {if(ch=='-') flag=0; ch=getchar();}
while(ch>='0'&&ch<='9') {X=(X<<1)+(X<<3)+ch-'0'; ch=getchar();}
if(flag) return X;
return ~(X-1);
}
inline void write(int X)
{
if(X<0) {X=~(X-1); putchar('-');}
if(X>9) write(X/10);
putchar(X%10+'0');
}
const int inf=0x3f3f3f3f;
const int N = 1e6+7;
struct Mo{int l,r,ID,k;int ans;}q[N];
int n,m,block,belong[N];
int sum[N],ans,num[N],a[N];
//bool cmp(Mo a,Mo b){return belong[a.l]==belong[b.l]?a.r<b.r:a.l<b.l;}
int cmp(Mo a, Mo b) {
return (belong[a.l] ^ belong[b.l]) ? belong[a.l] < belong[b.l] : ((belong[a.l] & 1) ? a.r < b.r : a.r > b.r);
}
bool CMP(Mo a,Mo b){return a.ID<b.ID;};
int mp[N],cnt[N],now,answer[N];
int exist[N];
signed main()
{
#ifndef ONLINE_JUDGE
freopen("IO\\in.txt","r",stdin);
freopen("IO\\out.txt","w",stdout);
#endif
n=read();
if (n>=500000) block=2300;
else
block=sqrt(n);
for (register int i=1;i<=n;i++)
a[i]=read(),belong[i]=(i-1)/block+1;
m=read();
for (register int i=1;i<=m;i++)
q[i].l=read(),q[i].r=read(),q[i].ID=i;
sort(q+1,q+m+1,cmp);
int l=1,r=0;
for (register int i=1;i<=m;i++)
{
while(l<q[i].l) now -= !--cnt[a[l++]];
while(l>q[i].l) now += !cnt[a[--l]]++;
while(r<q[i].r) now += !cnt[a[++r]]++;
while(r>q[i].r) now -= !--cnt[a[r--]];
answer[q[i].ID]=now;
}
for (register int i=1;i<=m;i++)
write(answer[i]),putchar('\n');
return 0;
}
推荐
莫队算法——从入门到黑题 - WAMonster - 博客园 (cnblogs.com)
总结
莫队算法主要是对离线的数据进行区间查询,但后人晚上了在线的,还有树上的操作,我还并未学到,将来慢慢补。