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\)):
思路
强制在线,所以无法使用莫队或扫描线统计 \(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
*/
:::

浙公网安备 33010602011771号