[luoguSP3267] D-query

题意

给定 \(n\) 个数 \(a_1 \cdots a_n\)\(q\) 个查询,每次查询区间 \([l,r]\) 中,\(a\) 的不同数的个数

sol

本题不强制在线,因此可以考虑离线算法,如莫队。
莫队是一个非常神奇的算法,它的基本思想是:将区间进行某种排序后,通过移动 \(l,r\) 指针,每次移动时处理答案的增减来得到答案。

如何排序

如果按照左右端点排序的话,左端点的移动次数为 \(O(n)\),但右端点的移动次数依然会被卡到 \(O(n^2)\),而莫队的思想是通过分块的方式,使左右端点移动次数均摊,即 \(O(n\sqrt n)\)
为此,需要将点按照 \(\sqrt n\) 个点一块的大小分块,然后按照左端点所在块为第一关键字,右端点为第二关键字进行排序。
通常来说,还会使用玄学的奇偶性优化进行卡常,即当左端点块为奇数时,右端点升序,否则右端点降序,虽然很玄学,但是会快很多,具体为什么我也不知道(

如何移动指针

当使用莫队时,往往在扩大区间时增加答案,在缩减区间时减小答案。而在某些题目中,当答案减小到负数时会出现一些莫名其妙的问题,因此在移动时我们需要先处理扩大区间,再处理缩小区间

对于本题,增减答案时只需要记录一个桶即可

代码

#include <iostream>
#include <algorithm>
#include <cstring>
#include <cmath>

using namespace std;

const int N = 30005, M = 200005, K = 1000005;

struct Ask{
    int l, r;
    int id;
} g[M];

int block[N], ans[M];
int n, q;
int a[N];
int res = 0;
int cnt[K];

bool cmp(Ask a, Ask b){
    if (block[a.l] != block[b.l]) return block[a.l] < block[b.l];
    if (block[a.l] & 1) return a.r < b.r;
    return a.r > b.r;
}

void del(int x){
    cnt[a[x]] -- ;
    if (!cnt[a[x]]) res -- ;
}

void add(int x){
    if (!cnt[a[x]]) res ++ ;
    cnt[a[x]] ++ ;
}

int main(){
    scanf("%d", &n);
    for (int i = 1; i <= n; i ++ ) scanf("%d", &a[i]);

    int sz = sqrt(n);
    int bcnt = ceil(1.0 * n / sz);

    for (int i = 1; i <= bcnt; i ++ )
        for (int j = (i - 1) * sz + 1; j <= i * sz; j ++ )
            block[j] = i;
    
    scanf("%d", &q);
    for (int i = 1; i <= q; i ++ ) scanf("%d%d", &g[i].l, &g[i].r), g[i].id = i;

    sort(g + 1, g + q + 1, cmp);

    int l = 1, r = 0;
    for (int i = 1; i <= q; i ++ ){
        int ql = g[i].l, qr = g[i].r;
        while (l > ql) add( -- l);
        while (r < qr) add( ++ r);
        while (l < ql) del(l ++ );
        while (r > qr) del(r -- );
        ans[g[i].id] = res;
    }

    for (int i = 1; i <= q; i ++ ) printf("%d\n", ans[i]);

    return 0;
}
posted @ 2024-11-23 22:28  是一只小蒟蒻呀  阅读(17)  评论(0)    收藏  举报