【普通莫队】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;
}

posted @ 2023-09-12 09:26  Mcggvc  阅读(16)  评论(0编辑  收藏  举报