P1972 【SDOI2009】 HH的项链

\(Description\)

给你一串长度为\(n\)的数,有\(m\)个询问,每次询问\(l-r\)内不相同的数的个数。

\(Solution\)

这道题显然不能直接通过线段树维护,考虑记录每一个数前一次出现的位置\(pos\),然后将这个值插入线段树,每次查询\(l\)在这个区间内的排名即可(第几小)。这个是比较显然的,因为假如一个数的前驱\(pos>=l\),说明在\(l\)后面至少还出现一次,而只有这个数在\(l-r\)内最靠前的才保证排名\(<l\)

所以问题就转换成了插入前驱数组,查询\(l-r\)区间内的\(l\)的排名
这显然还是不能使用线段树直接搞,考虑这是一个裸的主席树,只不过将查询第\(k\)变成查询\(k\)的排名

void query(int u,int v,int l,int r,int k)//k是权值 
{
    if(l==r)
    {
        if(l!=k) ans+=sums[v]-sums[u];//k就是询问区间L,假如某个数出现的位置是L,那么只统计在L处的即可
        //但要注意l!=k(L)时答案要加上答案,比如询问区间大小为1的情况,直接返回就炸了 
        return;
    }
    int num=sums[lc[v]]-sums[lc[u]];
    int mid=(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);
} 

然而呢

\(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]);//按编号输出 
}

猜你喜欢

转载自www.cnblogs.com/Liuz8848/p/11741055.html