从某种意义上说,莫队算法 属于那种理解起来比较抽象但是只要理解了就非常简单的一类算法,本质还是离不开暴力,适用于离线算法,如果要求是在线,则无法使用。
接下来贴一道经典的模板题https://www.luogu.com.cn/problem/P2709
题目描述
小B 有一个长为 𝑛 的整数序列 𝑎,值域为 [1,𝑘]。他一共有 𝑚 个询问,每个询问给定一个区间 [𝑙,𝑟],求:
其中 𝑐i 表示数字 𝑖 在 [𝑙,𝑟][l,r] 中的出现次数。
小B请你帮助他回答询问。
输入格式
第一行三个整数 𝑛,𝑚,𝑘。
第二行 𝑛 个整数,表示 小B 的序列。
接下来的 𝑚 行,每行两个整数 𝑙,𝑟。
输出格式
输出 𝑚 行,每行一个整数,对应一个询问的答案。
输入输出样例
输入
6 4 3 1 3 2 1 1 3 1 4 2 6 3 5 5 6
输出
6 9 5 2
说明/提示
【数据范围】
对于 100%100% 的数据,1≤𝑛,𝑚,𝑘≤5×104。
从这道题可以看出莫队算法对数组的区间操作非常的有优势,从暴力的角度,如果对于每一次查询都遍历一遍数组,那么时间复杂度会达到O(n2)。但是在查询的过程中,可以通过两个指针的左移右移操作,实现对区间的优化。
第一遍优化:
用指针的位移代替每一次重新遍历,优化大量时间。利用Add和Sub函数进行对新老目标的更新,计算其贡献。
第二遍优化:
但如果出现查询区间十分阴间的情况,例如[1,3][10,12][2,3],那么我们可以看见仅仅通过位移的方式依然没有任何优势,所以莫队算法使用了分块算法和双关键词排序的方法进一步优化,其中分块算法是把n长度的数组分成根号n的部分,并用pos记录下各个元素在哪一块部分
int siz = sqrt(n);
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
pos[i] = i / siz;
}//分块算法
struct node{
int l;
int r;
int id;
}q[N];
bool cmp(const node x, const node y)
{
if(pos[x.l] == pos[y.l])
return x.r < y.r;
return pos[x.l] < pos[y.l];
}//双关键词排序
通过这两步骤,可以智慧地把指针相近的查询先行操作,节省了指针跳来跳去的时间浪费,把时间复杂度减少到了O(n*n1/2).、
由此,我们可以得出莫队算法的基础模板,其中主函数是基本不变的模式,往往只需通过题意进行对Add和Sub函数的改变实现不同的问题解决。
一下是主函数部分:
int main()
{
cin >> n >> m >> k;
int siz = sqrt(n);
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
pos[i] = i / siz;
}
for(int i = 0; i < m; i ++)
{
cin >> q[i].l >> q[i].r;
q[i].id = i;
}
sort(q, q + m, cmp);
int l = 1, r = 0;
for(int i = 0; i < m; i ++)
{
while(q[i].l > l) Sub(l ++);
while(q[i].l < l) Add(-- l);
while(q[i].r > r) Add(++ r);
while(q[i].r < r) Sub(r --);
ans[q[i].id] = res;
}
for(int i = 0; i < m; i ++)
cout << ans[i] << endl;
}
除了Add和Sub函数,主函数中较难理解的是四段while语句,分别对应了当前指针与目标指针的对应位置而进行的操作,只有理解了这一部分就会觉得莫队算法非常简单。
回到例题,针对问题要求,我们可以用cnt数组存储各个数出现的次数,并进行对答案res的更新。下面是具体代码:
void Add(int n)
{
cnt[a[n]] ++;
res += (cnt[a[n]]) * (cnt[a[n]]) - (cnt[a[n]] - 1) * (cnt[a[n]] - 1);
}
void Sub(int n)
{
cnt[a[n]] --;
res -= (cnt[a[n]] + 1) * (cnt[a[n]] + 1) - (cnt[a[n]]) * (cnt[a[n]]);
}
附上完整代码:
#include <bits/stdc++.h>
using namespace std;
const int N = 1e6 + 10;
struct node{
int l;
int r;
int id;
}q[N];
int n, m, k, res = 0;
int a[N], pos[N], ans[N], cnt[N];
bool cmp(const node x, const node y)
{
if(pos[x.l] == pos[y.l])
return x.r < y.r;
return pos[x.l] < pos[y.l];
}
void Add(int n)
{
cnt[a[n]] ++;
res += (cnt[a[n]]) * (cnt[a[n]]) - (cnt[a[n]] - 1) * (cnt[a[n]] - 1);
}
void Sub(int n)
{
cnt[a[n]] --;
res -= (cnt[a[n]] + 1) * (cnt[a[n]] + 1) - (cnt[a[n]]) * (cnt[a[n]]);
}
int main()
{
cin >> n >> m >> k;
int siz = sqrt(n);
for(int i = 1; i <= n; i ++)
{
cin >> a[i];
pos[i] = i / siz;
}
for(int i = 0; i < m; i ++)
{
cin >> q[i].l >> q[i].r;
q[i].id = i;
}
sort(q, q + m, cmp);
int l = 1, r = 0;
for(int i = 0; i < m; i ++)
{
while(q[i].l > l) Sub(l ++);
while(q[i].l < l) Add(-- l);
while(q[i].r > r) Add(++ r);
while(q[i].r < r) Sub(r --);
ans[q[i].id] = res;
}
for(int i = 0; i < m; i ++)
cout << ans[i] << endl;
}