HDU5919 Sequence II题解(主席树+倒序建树运用)

前言

原题传送门

博客园 - 也许更好的阅读体验

好像没啥好说的,直击主题吧。

题解

简化题意

给定一个长度为 \(n\) 的序列 \(a\)\(m\) 次询问。第 \(i\) 次询问给定 \(l'_i\)\(r'_i\),通过转换得到 \(l_i\)\(r_i\)。设区间 \([l_i,r_i]\) 中不同的数字有 \(k_i\) 个,记 \(p_{i,1}\)\(p_{i,k_i}\) 为每个不同数字第一次出现的位置,其中 \(p_i\) 按升序排序。求 \(p_i\) 的中位数是多少,即求 \(p_{i,\lceil{\frac{k_i}{2}}\rceil}\)

转换如下 (\(ans\) 为上一次的答案,初始为 \(0\)):

\[l_i=\min((l'_i+ans_{i−1}) \bmod n + 1,(r'_i+ans_{i−1}) \bmod n + 1) \]

\[r_i=\max((l'_i+ans_{i−1}) \bmod n + 1,(r'_i+ans_{i−1}) \bmod n + 1) \]

思路

强制在线,所以无法使用莫队或扫描线统计 \(k_i\),只能考虑在线算法主席树。

参考求静态区间第 \(k\) 小的做法,可以想到对每个权值线段树的叶子打上一个标记 \(tag\),等于 \(1\) 时表示是否在区间内存在过,通过计算整棵线段树的权值和统计 \(k_i\)

但这样做就出现了问题:如果线段树 \(R\) 中存在一个数 \(x\),但需要减去的线段树 \(L-1\) 中同样存在 \(x\),两个版本 \(x\)\(tag\) 都为 \(1\),相减后 \(x\)\(tag\) 就变为了 \(0\),无法表示 \(x\) 存在于区间 \([L,R]\)

所以换一种方法,不使用权值线段树,直接维护每个位置的数是不是在某一个区间内首次出现。具体的,若询问区间左端点为 \(L\),只需要把 \(L\) 右边每个第一次出现的数的位置标记成 \(1\),在查询时计算 \([1,R]\)\([L,R]\))的区间和即可。

因此,需要倒序建立主席树,以保证线段树 \(L\) 对应的区间是 \([L,n]\)。对于新扩展的数 \(a_{L-1}\) 是否在右边出现过,用 map 记录一下每个数出现的位置,把原先的位置设为 \(0\) 即可。

实现

一点点细节:将原先位置设为 \(0\) 和将新位置设为 \(1\) 是两步操作,每次都需要建立一棵新的线段树。这里的解决方案是删除都记在 root_remove 上,添加和查询都使用 root_add。

:::success[代码]

#include<iostream>
#include<cstdio>
#include<cstring>
#include<map>
using namespace std;
const int N=2e5+5;
int n,m;
int a[N];
map <int,int> pl;
int rtr[N],rta[N],tid;//root_remove,root_add
struct Tree{
    int ls,rs;
    int cnt;
}tr[N<<6];
#define mid ((l+r)>>1)
void updata(int p){
    int ls=tr[p].ls;
    int rs=tr[p].rs;
    tr[p].cnt=tr[ls].cnt+tr[rs].cnt;
}
void build(int &p,int l,int r){
    p=++tid;
    if(l==r){
        tr[p].cnt=0;
        return ;
    }
    build(tr[p].ls,l,mid);
    build(tr[p].rs,mid+1,r);
    updata(p);
}
void remove(int lp,int &p,int l,int r,int x){
    p=++tid;
    tr[p]=tr[lp];
    if(l==r){
        tr[p].cnt=0;
        return ;
    }
    if(x<=mid) remove(tr[lp].ls,tr[p].ls,l,mid,x);
    else remove(tr[lp].rs,tr[p].rs,mid+1,r,x);
    updata(p);
}
void add(int lp,int &p,int l,int r,int x){
    p=++tid;
    tr[p]=tr[lp];
    if(l==r){
        tr[p].cnt=1;
        return ;
    }
    if(x<=mid) add(tr[lp].ls,tr[p].ls,l,mid,x);
    else add(tr[lp].rs,tr[p].rs,mid+1,r,x);
    updata(p);
}
int getdiff(int p,int l,int r,int x,int y){
    if(x<=l&&r<=y){
        return tr[p].cnt;
    }
    int ans=0;
    if(x<=mid) ans+=getdiff(tr[p].ls,l,mid,x,y);
    if(mid<y) ans+=getdiff(tr[p].rs,mid+1,r,x,y);
    return ans;
}
int query(int p,int l,int r,int k){//查询和求第k小差不多
    if(l==r) return l;
    int ls=tr[p].ls,rs=tr[p].rs;
    if(tr[ls].cnt>=k) return query(ls,l,mid,k);
    else return query(rs,mid+1,r,k-tr[ls].cnt);
}
#undef mid
void solve(){
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
    }
    build(rta[n+1],1,n);
    for(int i=n;i>=1;i--){//倒序建树
        //如果ai在右边出现过了,把那个位置删除(设为0),并添加新的位置
        if(pl[a[i]]){
            remove(rta[i+1],rtr[i],1,n,pl[a[i]]);
            add(rtr[i],rta[i],1,n,i);
        }
        else add(rta[i+1],rta[i],1,n,i);
        pl[a[i]]=i;
    }
    int ans=0,l,r,L,R;
    for(int i=1;i<=m;i++){
        scanf("%d%d",&l,&r);
        L=min((l+ans)%n+1,(r+ans)%n+1);
        R=max((l+ans)%n+1,(r+ans)%n+1);
        int k=getdiff(rta[L],1,n,1,R);
        //整数向上取整:ceil(x/y)=(x+y-1)/y
        ans=query(rta[L],1,n,(k+2-1)/2);
        printf("%d ",ans);
    }
    //多测不清空,十年OI___
    tid=0;
    for(int i=1;i<=n;i++) pl[a[i]]=0;
}
int main(){
    int T;
    scanf("%d",&T);
    for(int i=1;i<=T;i++){
        printf("Case #%d: ",i);
        solve();
        printf("\n");
    }

    return 0;
}
/*
2
5 2
3 3 1 5 4
2 2
4 4
5 2
2 5 2 1 2
2 3
2 4
*/

:::

posted @ 2026-07-14 22:18  Kx_Triumphs  阅读(7)  评论(0)    收藏  举报