P1972 【SDOI2009】 HH的项链
#$Description$
给你一串长度为$n$的数,有$m$个询问,每次询问$l-r$内不相同的数的个数。
#$Solution$
这道题显然不能直接通过线段树维护,考虑记录每一个数前一次出现的位置$pos$,然后将这个值插入线段树,每次查询$l$在这个区间内的排名即可(第几小)。这个是比较显然的,因为假如一个数的前驱$pos>=l$,说明在$l$后面至少还出现一次,而只有这个数在$l-r$内最靠前的才保证排名$>1;
if(k>mid) ans+=num,query(rc[u],rc[v],mid+1,r,k);//在右子树中注意加上排名
else query(lc[u],lc[v],l,mid,k);
}
```
然而呢
![](https://img2018.cnblogs.com/blog/1564177/201910/1564177-20191025215334451-1736364844.png)
$tm$这道题数据加强后主席树就$GG$了,因为这道题数据达到了$1e6$
主席树空间开销$nlogn$容易开不下
数组开太大加上主席树比较大的常数在极限数据下$GG$了
##结果我发现这道题正解竟然是离线+树状数组!
关键在于如何使得数组离线,发现这样一个性质
>对于若干个询问的区间$[l,r]$,如果他们的$r$都相等的话,那么项链中出现的同一个数字,一定是只关心出现在最右边的那一个的
比如\(1\ 3\ 5\ 2\ 1\),对于所有\(r=5\)的询问,因为第\(5\)个数是\(1\),所以前面所有的\(1\)都不用关心,靠右边的数一定是更“优”的
所以我们每次只需要靠右的数看他能不能更新前面的数即可。
考虑使用梳妆数组维护
具体实现是按照询问的\(r\)排序,不停往右扫描,记录一个数上一次出现的位置,每次都在当前位置插入\(1\),如果出现过就在上一个出现的位置插入\(-1\)以消除上面的数的影响,扫描到一个\(r\)就进行区间求和,最后按编号输出答案就行
具体实现如下,一个简单的区间查询+单点修改
\(Code\)
#include<cstdio>
#include<iostream>
#include<algorithm>
#define maxn 1000010
#define re register
using namespace std;
inline int read()
{
int x=0,f=1; char ch=getchar();
while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
return x*f;
};
struct P{
int l,r,id;
}node[maxn];
int tre[maxn<<2];
int tmp,n,m,ans[maxn],a[maxn],vis[maxn];
bool cmp(P A,P B)
{
return A.r<B.r;
}
void add(int x,int k)
{
while(x<=n)
{
tre[x]+=k;
x+=(x&-x);
}
}
int query(int x)
{
int tmp2=0;
while(x>=1)
{
tmp2+=tre[x];
x-=(x&-x);
}
return tmp2;
}
int Query(int l,int r)//树状数组单点修改+区间查询
{
return query(r)-query(l-1);
}
int main()
{
n=read();
for(re int i=1;i<=n;++i) a[i]=read();
m=read();
for(re int i=1;i<=m;++i)
{
node[i].l=read(),node[i].r=read(),node[i].id=i;
}
sort(node+1,node+m+1,cmp);
tmp=1;
for(re int i=1;i<=m;++i)
{
for(re int j=tmp;j<=node[i].r;++j)//按r排序的原因上面说过
{
if(vis[a[j]])
add(vis[a[j]],-1);
add(j,1);//不管什么时候这里都要加
vis[a[j]]=j;
}
tmp=node[i].r+1;//记录下一次开始的位置,保证扫描是$O(n)$的
ans[node[i].id]=Query(node[i].l,node[i].r);//记录对应编号的答案
}
for(re int i=1;i<=m;++i)
printf("%d\n",ans[i]);//按编号输出
}