莫队算法
一句话算法:莫队是一种基于分块和询问排序思想的序列处理算法,因此大部分时间,我们需要离线询问,再对询问按某些优先级排序处理。
例题
详情见小Z的袜子,推导过程与莫队无关故略去,这里直接给出结论:
对于一个询问([l, r]),(ans = frac{sumlimits_{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(nsqrt 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
", ans1[i], ans2[i]);
return 0;
}