莫队算法学习笔记

莫队算法

一句话算法:莫队是一种基于分块和询问排序思想的序列处理算法,因此大部分时间,我们需要离线询问,再对询问按某些优先级排序处理。

例题

详情见小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;
}
posted @ 2019-12-13 17:20  kma_093  阅读(135)  评论(0编辑  收藏  举报