bzoj3339 Rmq Problem
题意
区间查询mex(minimum exclusive,最小的未在这个区间内出现的非负整数).
区间长度,询问次数,数字权值均为\(2*10^5\)
分析
这个题做法还是比较多的,离线后线段树扫一遍\(O(nlogn)\),莫队+树状数组\(O(n\sqrt{n}logn)\),莫队+分块\(O(n\sqrt{n})\).
一开始我只会复杂度最爆炸的莫队+树状数组
看别人博客有一种非常妙的主席树做法,时间\(O(nlogn)\),而且是在线的,于是学习了一波(感受自己的愚蠢).
好像网上题解写得比较多的那个离线线段树可以强行可持久化就在线了?
好像这个做法也可以改造成离线
考虑对某个询问\([L,R]\)二分答案.
如何判断区间的mex是否大于ans?我们只需要判断[L,R]是否出现了\([1,ans]\)之中所有的权值.
考虑\([1,ans]\)的每个权值在\(A[1...R]\)中最靠右的出现位置.如果不存在这个权值那么最靠右的出现位置就是0.
例如某个权值x在R左侧的最靠右的出现位置为y,那么这表示右端点为R的区间如果想要包含权值x,左端至少要延伸到y.因此,如果\(L>y\),那么\([L,R]\)区间内不包含x这个权值.
那么把权值x在R左侧的最靠右的出现位置记作\(G(x)\),对于二分的答案ans,我们判断是否对于\(G(0),G(1),G(2)...G(ans)\),都有\(G(i)>=L\).那么这是一个前缀查询,可以用线段树处理.而我们并不需要二分答案再每次在线段树上查询的两个log,可以直接在线段树上二分出答案,每次考虑答案在线段树节点的左半部分还是右半部分.
然后离线,把询问按照右端点排序扫一遍就可以了.
把线段树可持久化就可以在线了.
#include<cstdio>
#include<algorithm>
using namespace std;
const int maxn=200005;
struct node{
node* ch[2];
int Min;
node(){}
node(int x){
Min=x;ch[0]=ch[1]=0;
}
}t[maxn*40];int tsz=0;
node* newnode(int x){
t[++tsz]=node(x);return t+tsz;
}
void Insert(node* rt0,node* &rt,int l,int r,int k,int x){
rt=newnode(0);
if(l==r){
rt->Min=x;
}else{
int mid=(l+r)>>1;
if(k<=mid){
Insert(rt0->ch[0],rt->ch[0],l,mid,k,x);
rt->ch[1]=rt0->ch[1];
}else{
Insert(rt0->ch[1],rt->ch[1],mid+1,r,k,x);
rt->ch[0]=rt0->ch[0];
}
rt->Min=min(rt->ch[0]->Min,rt->ch[1]->Min);
}
}
int query(node* rt,int l,int r,int lim){
if(l==r){
return l;
}
int mid=(l+r)>>1;
if(rt->ch[0]->Min<lim)return query(rt->ch[0],l,mid,lim);
else return query(rt->ch[1],mid+1,r,lim);
}
node* root[maxn];
int a[maxn];
int main(){
int n,q;scanf("%d%d",&n,&q);
for(int i=1;i<=n;++i)scanf("%d",a+i);
root[0]=newnode(0);
root[0]->ch[0]=root[0]->ch[1]=root[0];
for(int i=1;i<=n;++i){
Insert(root[i-1],root[i],0,200000,a[i],i);
}
int l,r;
for(int i=1;i<=q;++i){
scanf("%d%d",&l,&r);
printf("%d\n",query(root[r],0,200000,l));
}
return 0;
}