【普通莫队】2023牛客多校5 A
简介
莫队算法是由莫涛提出的算法。
在莫涛提出莫队算法之前,莫队算法已经在 Codeforces 的高手圈里小范围流传,但是莫涛是第一个对莫队算法进行详细归纳总结的人。
莫涛提出莫队算法时,只分析了普通莫队算法,但是经过 OIer 和 ACMer 的集体智慧改造,莫队有了多种扩展版本。
莫队算法可以解决一类离线区间询问问题,适用性极为广泛。同时将其加以扩展,便能轻松处理树上路径询问以及支持修改操作。
—— OI-Wiki
普通莫队
例题:给定一个长为n的序列 \(a_1, a_2, ..., a_n\),有m次查询,每次查询求区间 \([l, r]\)中满足: \(l \leq i < j < k \leq r\),\(a_i = a_k > a_j\)的三元组\((i, j, k)\)的个数。
莫队用于处理这样的问题:离线多次查询序列区间,每次查询都需要\(O(n)\)的时间计算答案,但是由区间\([l, r]\)的答案计算\([l + 1, r], [l - 1, r], [l, r + 1], [l, r - 1]\)的答案时只需要\(O(1)\)的时间。
当已知\([l, r]\)的答案,要计算\([l^{'}, r^{'}]\)的答案时,只需将\(l\)转移到\(l^{'}\), \(r\)转移到\(r^{'}\),将区间两个端点看成平面坐标系上的一个点的横纵坐标,转移过程所需要的步数即为两点之间的曼哈顿距离。
由此,我们可以将所有点在平面坐标系中画出来,最佳的转移方案即为一个最小生成树,其中两点之间的距离为曼哈顿距离,转移所需的步数为最小生成树的大小。
考虑对查询区间进行如下的排序方式,可以获得较优的转移顺序:
将序列按照\(\sqrt n\)大小分块,从左到右标上序号,按照查询区间左端点所属分块序号为第一关键字,右端点大小为第二关键字排序,最终在\(n=m\)的情况下计算所有答案的时间复杂度为\(O(n \sqrt n)\)
给出一个不太严谨的证明:
首先分块之后,每个块大的大小为\(\sqrt n\),块的数量为\(\lceil \sqrt n \rceil\),暴力计算每个块的第一个查询区间,每次计算\(O(n)\),共有\(\lceil \sqrt n \rceil\)次,时间复杂度为\(O(n \sqrt n)\)。在同一个块中,右端点单调递增,转移右端点的复杂度为\(O(n)\),左端点每次转移都在同一块中,复杂度为\(O(\sqrt n)\),转移的总数为\(n\)次,总复杂度为\(O(n \sqrt n)\)。
对于上面的例题,首先用树状数组求出每个位置\(i\)前面满足\(a_j < a_i\)的\(j\)的数量,记为\(c_i\)。为了\(O(1)\)转移,还需记录两个数组\(tot_i\)和\(sum_i\),分别表示当前区间中值为\(i\)的位置的数量和总和。
分四种情况:
\(r\)到\(r + 1\),答案增加\(tot_{a_{r + 1}} \cdot c_{r + 1} - sum_{a_{r + 1}}\)
\(l\)到\(l - 1\),答案增加\(sum_{a_{l - 1}} - tot_{a_{l - 1}} \cdot c_{l - 1}\)
\(r\)到\(r - 1\),答案减少\((tot_{a_r} - 1) \cdot c_r - (sum_{a_r} - c_r)\)
\(l\)到\(l + 1\),答案减少\((sum_{a_l} - c_l) - (tot_{a_l} - 1) \cdot c_l\)
代码
#include <bits/stdc++.h>
using namespace std;
typedef long long lld;
const int N = 500005;
struct Que {
int l, r, num;
lld ans;
};
int n, m, a[N];
lld c[N], tot[N], sum[N];
vector <int> poss[N];
lld s[N];
int siz, id[N], numb;
Que que[N];
inline int lowbit(int x) {
return x & -x;
}
void add(int x, int k) {
for(; x < N; x += lowbit(x)) s[x] += k;
}
int Sum(int x) {
int summ = 0;
for(; x; x -= lowbit(x)) summ += s[x];
return summ;
}
bool cmp1(Que a, Que b) {
if(id[a.l] == id[b.l]) return a.r < b.r;
return id[a.l] < id[b.l];
}
bool cmp2(Que a, Que b) {
return a.num < b.num;
}
int main() {
// freopen("data.in", "r", stdin);
ios::sync_with_stdio(false);
cin >> n >> m;
siz = (int)sqrt(n); numb = ceil((double)n / (double)siz);
for(int i = 1; i <= numb; i++) {
for(int j = (i - 1) * siz + 1; j <= i * siz; j++) {
id[j] = i;
}
}
for(int i = 1; i <= n; i++) {
cin >> a[i];
poss[a[i]].push_back(i);
}
for(int i = 1; i <= m; i++) {
cin >> que[i].l >> que[i].r;
que[i].num = i;
}
for(int i = 1; i <= n; i++) {
if(poss[i].size()) {
for(auto j : poss[i]) {
c[j] = Sum(j);
}
for(auto j : poss[i]) {
add(j, 1);
}
}
}
sort(que + 1, que + 1 + m, cmp1);
int l = 1, r = 0; lld ans = 0;
for(int i = 1; i <= m; i++) {
int nowl = que[i].l, nowr = que[i].r;
while(l < nowl) {
ans = ans - ((sum[a[l]] - c[l]) - (tot[a[l]] - 1) * c[l]);
tot[a[l]]--; sum[a[l]] -= c[l]; l++;
}
while(l > nowl) {
ans = ans + (sum[a[l - 1]] - tot[a[l - 1]] * c[l - 1]);
tot[a[l - 1]]++; sum[a[l - 1]] += c[l - 1]; l--;
}
while(r < nowr) {
ans = ans + (tot[a[r + 1]] * c[r + 1] - sum[a[r + 1]]);
tot[a[r + 1]]++; sum[a[r + 1]] += c[r + 1]; r++;
}
while(r > nowr) {
ans = ans - ((tot[a[r]] - 1) * c[r] - (sum[a[r]] - c[r]));
tot[a[r]]--; sum[a[r]] -= c[r]; r--;
}
que[i].ans = ans;
}
sort(que + 1, que + 1 + m, cmp2);
for(int i = 1; i <= m; i++) {
cout << que[i].ans << endl;
}
return 0;
}