莫队
莫队算法是一种离线算法,不强制在线,通常有多次区间询问。
通过对询问进行排序,区间的伸长缩短来实现。
简而言之:莫队=离线+暴力+分块。
问题引入
放道题:
luogu_P2709
题目简意:
有一个长为
n
n
n 的整数序列
a
a
a,值域为
[
1
,
k
]
[1,k]
[1,k]。
共有
m
m
m 个询问,每个询问给定一个区间
[
l
,
r
]
[l,r]
[l,r],求:
∑
i
=
1
k
c
i
2
\sum\limits_{i=1}^k c_i^2
i=1∑kci2
其中
c
i
c_i
ci 表示数字
i
i
i 在
[
l
,
r
]
[l,r]
[l,r] 中的出现次数。
乍看之下我好像只会暴力啊?
假设我们算完 [ L , R ] [L,R] [L,R] 的答案后现在要算 [ L ′ , R ′ ] [L',R'] [L′,R′] 的答案。由于可以在 O ( 1 ) O(1) O(1) 的时间下得到 [ L , R + 1 ] , [ L , R − 1 ] , [ L + 1 , R ] , [ L − 1 , R ] [L,R+1],[L,R-1],[L+1,R],[L-1,R] [L,R+1],[L,R−1],[L+1,R],[L−1,R] 的答案,所以计算 [ L ′ , R ′ ] [L',R'] [L′,R′] 的答案耗时 ∣ L − L ′ ∣ + ∣ R − R ′ ∣ |L-L'|+|R-R'| ∣L−L′∣+∣R−R′∣,如果把 [ L , R ] [L,R] [L,R] 和 [ L ′ , R ′ ] [L',R'] [L′,R′] 都看成平面上的点的话,那么时间开销就是两点的曼哈顿距离。
具体实现:
- 先对序列进行分块操作,每块的长度为 n \sqrt n n;
- 然后以询问左端点所在的块为第一关键字,右端点大小为第二关键字进行排序,每个区间是由上一区间递推得到,所以单次询问时间复杂度是 O ( n ) O(n) O(n);
- 但是在块中每次询问 L L L 的移动最大为 n \sqrt n n, R R R 的移动总和最大为 n n n,块与块之间的移动最大为 n n n,所以总复杂度为 O ( ( n + m ) n ) O((n+m)\sqrt n) O((n+m)n)。
代码:
#include <iostream>
#include <cstdio>
#include <algorithm>
#include <cmath>
using namespace std;
#define maxn 50005
#define int long long
struct node {
int l, r, id, pos;
} a[maxn];
bool cmp(node x, node y) {
if (x.pos == y.pos) return x.r < y.r;
return x.pos < y.pos;
}
int b[maxn], n, m, K;
int cnt[maxn], Ans[maxn];
signed main() {
ios::sync_with_stdio(false);
cin.tie(0);
cin >> n >> m >> K;
int size = (int)sqrt(n);
for (int i = 1; i <= n; i++) cin >> b[i];
for (int i = 1; i <= m; i++) {
cin >> a[i].l >> a[i].r;
a[i].id = i;
a[i].pos = (a[i].l - 1) / size + 1;
}
sort(a + 1, a + m + 1, cmp);
int l = 1, r = 0;
int ans = 0;
for (int i = 1; i <= m; i++) {
while (l > a[i].l) {
l--;
cnt[b[l]]++;
ans += 2 * cnt[b[l]] - 1;
}
while (r < a[i].r) {
r++;
cnt[b[r]]++;
ans += 2 * cnt[b[r]] - 1;
}
while (l < a[i].l) {
cnt[b[l]]--;
ans -= 2 * cnt[b[l]] + 1;
l++;
}
while (r > a[i].r) {
cnt[b[r]]--;
ans -= 2 * cnt[b[r]] + 1;
r--;
}
Ans[a[i].id] = ans;
}
for (int i = 1; i <= m; i++) cout << Ans[i] << '\n';
}