莫队算法学习笔记
莫队算法
一句话算法:莫队是一种基于分块和询问排序思想的序列处理算法,因此大部分时间,我们需要离线询问,再对询问按某些优先级排序处理。
例题
详情见小Z的袜子,推导过程与莫队无关故略去,这里直接给出结论:
对于一个询问\([l, r]\),\(ans = \frac{\sum\limits_{i = 1}^{C}cnt[i] * (cnt[i] - 1)}{(r - l + 1)(r - l)}\),其中\(C\)为颜色的值域
考虑普通暴力:
维护两个指针\(l,r\),表示当前处理的区间,每次挪动两指针来靠近询问并修改当前答案
挪动的时候减掉和加上当前点的贡献的操作相信大家都会,比如对于这个例题,我要向左向右挪动一位,那么对应的程序是这样的
void modify(int k, int d) {
ans -= cnt[col[k]] * (cnt[col[k]] - 1);
cnt[col[k]] += d;
ans += cnt[col[k]] * (cnt[col[k]] - 1);
}
其中\(d\)为\(1/-1\),表示此次挪动是添加还是删除
显然这样我们还是会被毒瘤出题人卡到\(O(nm)\),比如如果每次询问都需要把\(l,r\)指针横跨整个序列地挪动
考虑对于询问,我们要怎样处理,才能够减小总的时间复杂度,而不是寄希望于数据湿度上呢
于是莫涛大神提出了一个算法,可以将处理这类问题的复杂度优化到\(O(n\sqrt n)\)
莫队算法就此产生
算法流程
首先我们离线所有询问,并把序列按某种方式分块。
对于我们得到的询问,按第一关键字为左端点所属块的序号,第二关键字为右端点的位置排序。
对于我们得到的询问,再按暴力的方式去处理它
这样做就有用吗?对于时间复杂度的优化程度如何?
下面给出证明。
时间复杂度证明
首先对于一次挪动,答案的更新是\(O(1)\)的
对于\(l\)和\(r\)指针的挪动分别讨论:
设块大小为\(base\)
- 对于\(l\)指针的挪动,当它不需要跨块的时候,它的单次最坏复杂度是\(O(base)\)的,而需要跨块的时候单次最坏复杂度是\(O(2 * base)\)的,常数\(2\)可以忽略不计,于是对于总共\(m\)次的挪动,\(l\)的挪动是\(O(m*base)\)级别的。
- 对于\(r\)指针的挪动,由于\(r\)指针在对应的\(l\)指针在同一块内是有序的,故对于每一块内的\(l\),右端点的挪动最坏情况是\(O(n)\),而左端点最劣是从头到尾一共\(\frac{n}{base}\)块都挪一遍,所以\(r\)指针的挪动是\(O(n * \frac{n}{base})\)
左右指针的挪动互不干涉,故总的时间复杂度是两者相加,即\(O(m * base + n * \frac{n}{base})\)
当\(m, n\)同阶时,\(base\)取\(\sqrt n\)有最优时间复杂度\(O(2 * n * \sqrt n)\)
本题代码:
#include<bits/stdc++.h>
#define N (100000 + 10)
using namespace std;
typedef long long ll;
int col[N], n, m, pos[N];
ll cnt[N], ans;
ll ans1[N], ans2[N];
inline int read() {
int cnt = 0, f = 1; char c = getchar();
while(!isdigit(c)) {if (c == '-') f = -f; c = getchar();}
while(isdigit(c)) {cnt = (cnt << 3) + (cnt << 1) + (c ^ 48); c = getchar();}
return cnt * f;
}
struct Q {
int l, r, id;
}q[N];
bool cmp (Q a, Q b) {
return (pos[a.l] == pos[b.l]) ? a.r < b.r : pos[a.l] < pos[b.l];
}
ll gcd(ll a, ll b) {return b ? gcd(b, a % b) : a;}
void modify(int k, int d) {
ans -= cnt[col[k]] * (cnt[col[k]] - 1);
cnt[col[k]] += d;
ans += cnt[col[k]] * (cnt[col[k]] - 1);
}
int main() {
n = read(), m = read();
for (register int i = 1; i <= n; ++i) col[i] = read();
for (register int i = 1; i <= m; ++i) q[i].l = read(), q[i].r = read(), q[i].id = i;
int base = sqrt(n);
for (register int i = 1; i <= n; ++i) pos[i] = i / base;
sort (q + 1, q + m + 1, cmp);
int L = 1, R = 1;
++cnt[col[1]];
for (register int i = 1; i <= m; ++i) {
if (q[i].l == q[i].r) {ans1[q[i].id] = 0, ans2[q[i].id] = 1; continue;}
while (R < q[i].r) modify(++R, 1);
while (R > q[i].r) modify(R--, -1);
while (L < q[i].l) modify(L++, -1);
while (L > q[i].l) modify(--L, 1);
ll len = q[i].r - q[i].l + 1;
ll tot = len * (len - 1);
ll g = gcd(ans, tot);
ans1[q[i].id] = ans / g, ans2[q[i].id] = tot / g;
}
for (register int i = 1; i <= m; ++i) printf("%lld/%lld\n", ans1[i], ans2[i]);
return 0;
}