题解:P2770 [SDOI2009] HH的项链

题意

一个长度为 \(n\) 的序列 \(a_i\)\(m\) 次询问,每次给出一个区间 \([l,r]\),问这个区间内有多少个不同的 \(a_i\)

做法

做法 1

碰到这种询问题,优先考虑数据结构或者离线,区间查询问题的一个常见套路是莫队 (话说这不是莫队裸题),先考虑静态区间下如何高效求出一个区间内不同元素的个数,容易想到逛画展那道题双指针的思路,如果当前扩展了一个之前区间内没有的,那么不同的元素个数需要 \(+1\),如果删除了一个之前仅有一个的,不同元素个数要 \(-1\),(其余清空不用考虑因为对答案没有任何影响),考虑如何计,开一个桶,记录每个元素有多少个,然后按照上面的处理即可。那这一段对答案有什么用呢?我们想:利用上面的,我们可以将一个区间的答案扩展到另外一个区间的答案,那么我们就可以先把所有的询问离线下来,然后找到一个排序方式使得指针总移动次数最少:

  • 依次处理:显然很慢,当我构造出询问为 \([1,1],[n,n],[1,1],\cdots,[n,n]\) 这样的询问,算法会被卡成 \(O(n^2)\)。那有同学说:我做一个记忆化,不就好了?实际根本不行,先不说你 \(O(n^2)\) 的数组是否能开的下,如果询问为:\([1,1],[n,n],[2,2],[n-1,n-1],\cdots\) 也可以卡你,总的来说有一万种卡你的方式。
  • 按照左端点排序:依旧不够优秀,虽然你学聪明了减少了左端点 \(l\) 的跨度,但是我只要去 \([1,n],[1,2],\cdots\) 也可以卡,所以我们需要一个更加优秀的排序算法。
  • 莫队算法的排序:这也是莫队的精髓,运用分块思想,一般块长为 \(\sqrt{n}\),我们暂且称一个询问左端点所在块为 \(jobl\),右端点所在块为 \(jobr\)。是这样排序的:我们优先比较 \(jobl\),如果 \(jobl\) 相等则比较 \(jobr\)。这样为什么是对的?证明请看 oi-wiki,证明篇幅过长,我觉得 oi-wiki 的证明写的非常好理解。

于是你高兴的写下了如此代码:

#include<bits/stdc++.h>
using namespace std;
const int N=1e6+5;
inline int read(){
    int x=0,f=1;
    char ch=getchar_unlocked();
    while (!isdigit(ch)){
        if (ch=='-') 
            f=-1;
        ch=getchar_unlocked();
    }
    while (isdigit(ch)){
        x=(x<<1)+(x<<3)+(ch^48);
        ch=getchar_unlocked();
    }
    return x*f;
}
inline void write(int x){
    if (x<0)putchar('-'),x=-x;
    if (x>9)write(x/10);
    putchar(x%10+'0');
}
int cnt[N],ans[N],c[N];
int n,m,maxn,sum;
struct query{
    int l,r,id;
    bool operator<(const query &x)const{
        if (l/maxn!=x.l/maxn)return l<x.l;
        else return (l/maxn)&1?r<x.r:r>x.r;
    }
}a[N];

void del(int i){
    if (cnt[c[i]]==1)--sum;
    --cnt[c[i]];
}
void add(int i){
    if (cnt[c[i]]==0)++sum;
    ++cnt[c[i]];
}
int main(){
    n=read();
    for (int i=1;i<=n;++i)c[i]=read();
    m=read();
    maxn=sqrt(n);
    for (int i=1;i<=m;++i){
        a[i].l=read();
        a[i].r=read();
        a[i].id=i;
    }
    sort(a+1,a+m+1);
    for (int i=1,l=1,r=0;i<=m;++i){
        while (l<a[i].l)del(l++);
        while (r>a[i].r)del(r--);
        while (l>a[i].l)add(--l);
        while (r<a[i].r)add(++r);
        ans[a[i].id]=sum;
    }
    for (int i=1;i<=m;++i){
        write(ans[i]);
        putchar('\n');
    }
    return 0;
}

发现 T 飞了,考虑一些卡常优化,具体的可以看这个帖子

然后核桃的机子可能没有洛谷那么强劲,洛谷上稳过的代码到了核桃需要再进行一些卡常,加读入优化,需要加上这个快读

做法 2

教练,我不想卡常!

当然可以,先考虑一个性质:对于任意区间的答案贡献,我们可以仅关注区间内每个数最右边的一个。然后根据这个性质,先把所有的询问离线下来,然后右端点排序,树状数组/线段树直接做即可。很好写所以代码不放。

做法 3

丸辣!要求强制在线。

在线,很难不想到主席树。关注到之前得到的性质,我们可以处理出 \(nxt_i\),表示 \(i\) 这个位置的数下一次会在什么地方出现,数组记一下然后遍历一遍就可以求出来。

考虑查询的本质,就是求所有满足 \(l\le i\le r,nxt_{i}>r\)\(i\) 的个数,很好理解,问题被转化为:

  • 每次给定一个区间 \([l,r]\),求值(也就是 \(nxt\))大于 \(r\) 的个数。

拿个主席树做就好了。

posted @ 2025-08-22 13:26  Cefgskol  阅读(11)  评论(0)    收藏  举报