线段树与MEX问题
可持久化线段树与MEX问题
典例
看这样一个经典的问题:给定序列 \(A\),和若干次询问 \([l,r]\),每次要求求出 \({a_l,a_{l+1},a_{l+2},...,a_r}\) 中的 MEX。
例题P4137
情况1
如果说序列 \(A\) 自己构成一个 permutation 或者 内部元素不重复,那么直接对下标扫描线,并且建立一个可持久化权值线段树,这样就容易通过在权值线段树上二分来找到第一个为 \(0\) 的位置。判断答案是否在一个线段树节点内的方式是去检查是否 val[x]==r-l+1,然后就按照线段树二分的方式递归查找即可。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int val[N<<5],ls[N<<5],rs[N<<5];
int root[N],node;
inline void pu(int x){val[x]=val[ls[x]]+val[rs[x]];}
//0-2e5+1
inline void modify(int pre,int &x,int l,int r,int pos)
{
val[x=++node]=val[pre],ls[x]=ls[pre],rs[x]=rs[pre];
if(l==r)return val[x]++,void();
int mid=l+r>>1;
if(pos<=mid)modify(ls[pre],ls[x],l,mid,pos);
else modify(rs[pre],rs[x],mid+1,r,pos);
pu(x);
}
inline int query(int x,int y,int l,int r)
{
if(l==r)return val[y]-val[x]==0 ? l : -1;
int mid=l+r>>1;
if(val[ls[y]]-val[ls[x]]<mid-l+1)return query(ls[x],ls[y],l,mid);
else if(val[rs[y]]-val[rs[x]]<r-mid)return query(rs[x],rs[y],mid+1,r);
else return -1;
}
int n,m;
int a[N];
inline void solve()
{
cin>>n>>m;
for(int i=1;i<=n;++i)cin>>a[i];
for(int i=1;i<=n;++i)modify(root[i-1],root[i],0,2e5+1,a[i]);
for(int i=1,l,r;i<=m;++i)
{
cin>>l>>r;
int ans=query(root[l-1],root[r],0,2e5+1);
cout<<ans<<'\n';
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
return solve(),0;
}
情况2
如果不保证元素不重复,那么这种做法就失去了正确性,因为我们可以默认 “\(val[x]==r-l+1\)” 与 "区间全满" 等价的前提就是元素互不相同,否则就可能造成区间不满,但是仍然有 \(val[x]==r-l+1\) 成立的情况出现。
这时应该怎么做呢?回顾在 区间静态数颜色 的经典问题 HH的项链 中我们用到的 trick,对于右端点扫描线,然后把所有出现过的数尽可能地“挂”在最右边,举个例子,如果 \(v\) 目前出现在了 \({1,3,4,5}\) 这些位置上面,那么我们记录 \(pos_v\) 为 \(5\),这样能保证当给出一个以 \(l\) 为左端点的查询时,不会漏掉任何一个答案,也不会算重(因为每个数只计算了一次)。
回到当前求 \([l,r]\) 的 MEX 的问题中,我们同样对于询问的右端点扫描线,这时对于其查询的左端点 \(l\),我们就是要找到:在当前出现过的数中,满足 出现的最右位置 \(P<l\) 的最小数,我们同样对于值域建立主席树,不同之处在于,我们现在维护的是每个数出现的最右位置,这就转化成了 线段树二分 能够解决的经典问题,在前面的文章中已经提到过,所以不在赘述了,代码如下。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=2e5+10;
int val[N<<5],ls[N<<5],rs[N<<5];
int root[N],node;
inline void pu(int x){val[x]=min(val[ls[x]],val[rs[x]]);}
//0-2e5+1
//维护前缀里面每个数最后出现的位置
inline void modify(int pre,int &x,int l,int r,int pos,int v)
{
val[x=++node]=val[pre],ls[x]=ls[pre],rs[x]=rs[pre];
if(l==r)return val[x]=v,void();
int mid=l+r>>1;
if(pos<=mid)modify(ls[pre],ls[x],l,mid,pos,v);
else modify(rs[pre],rs[x],mid+1,r,pos,v);
pu(x);
}
//[l,r]:最靠左的满足 val < l 的位置
inline int query(int x,int l,int r,int v)
{
if(l==r)return val[x]<v ? l : -1;
int mid=l+r>>1;
if(val[ls[x]]<v)return query(ls[x],l,mid,v);
else if(val[rs[x]]<v)return query(rs[x],mid+1,r,v);
else return -1;
}
int n,m;
int a[N];
inline void solve()
{
cin>>n>>m;
for(int i=1;i<=n;++i)cin>>a[i];
for(int i=1;i<=n;++i)modify(root[i-1],root[i],0,2e5+1,a[i],i);
for(int i=1,l,r;i<=m;++i)
{
cin>>l>>r;
cout<<query(root[r],0,2e5+1,l)<<'\n';
}
}
int main()
{
ios::sync_with_stdio(0);
cin.tie(0);
return solve(),0;
}
HH的项链在线做法
众所周知,上述提到的 HH的项链 的做法是离线的,那么是否可以用在线做法解决这道题呢?答案是可以的。对于下标建立一个权值线段树,然后对于每个前缀都记录一个历史版本,把每个数左右边的出现位置下标对应的权值 \(+1\) ,这样就转化成了普通的区间求和问题。但是主席树的常数有点太大了,死命卡常也还剩个点过不了,但至少正确性是能保证的。
Code
#include<bits/stdc++.h>
using namespace std;
const int N=1e6+10;
int a[N],root[N],pos[N],n,m,node;
int val[N<<5],ls[N<<5],rs[N<<5];
inline void pu(int x){val[x]=val[ls[x]]+val[rs[x]];}
inline int read()
{
int x = 0, f = 1;
char ch = getchar();
while (!isdigit(ch))
{
if (ch == '-') f = -1;
ch = getchar();
}
while (isdigit(ch))
{
x = x * 10 + ch - '0';
ch = getchar();
}
return x * f;
}
inline void write(int x)
{
if (x < 0)
{
putchar('-');
x = -x;
}
if (x >= 10) write(x / 10);
putchar(x % 10 + '0');
}
inline void modify(int pre,int &x,int l,int r,int pos,int v)
{
val[x=++node]=val[pre],ls[x]=ls[pre],rs[x]=rs[pre];
if(l==r)return val[x]+=v,void();
int mid=l+r>>1;
if(pos<=mid)modify(ls[pre],ls[x],l,mid,pos,v);
else modify(rs[pre],rs[x],mid+1,r,pos,v);
val[x]=val[ls[x]]+val[rs[x]];
}
int ql,qr;
inline int query(int x,int l,int r)
{
if(ql<=l&&r<=qr)return val[x];
int mid=l+r>>1;
int ans=0;
if(ql<=mid)ans+=query(ls[x],l,mid);
if(qr>mid)ans+=query(rs[x],mid+1,r);
return ans;
}
#define R register
inline void Q()
{
ql=read(),qr=read();
write(query(root[qr],1,n)),putchar('\n');
}
inline void solve()
{
n=read();
for(R int i=1;i<=n;++i)
{
a[i]=read();
root[i]=root[i-1];
if(pos[a[i]])modify(root[i],root[i],1,n,pos[a[i]],-1);
pos[a[i]]=i;
modify(root[i],root[i],1,n,pos[a[i]],1);
}
m=read();
for(R int i=1;i+3<=m;i+=4)
Q(),Q(),Q(),Q();
for(R int i=1;i<=m%4;++i)
Q();
}
int main()
{
return solve(),0;
}
区间操作与MEX
如题,区间操作和MEX都是可以用线段树来维护的东西,例题 CF817F。
区间数字引入和删除等同于全部赋值为 \(0\) 和 \(1\),区间反转可以直接在 \(tag\) 上进行 \(\oplus\) 操作,查询 MEX 相当于在线段树上二分找第一个为 \(0\) 的位置,这样我们就可以把这个抽象的题目变成常规线段树操作了。
但是这题的值域超大,明显需要离散化,有一个问题是:两个本不连续的区间 \([l_1,r_1],[l_2,r_2]\) 在离散化之后会变得相邻,这样本可能在之间出现的答案反而被我们忽略掉了,所以这题在离散化的时候应该把 \(1,l_i,r_i,r_i+1\) 都加入离散化的行列里(\(1\) 也可能是最后的答案)。
除此之外,多 \(tag\) 的线段树一向都是比较烧脑的,这里十分推荐用面向对象编程的思想封装,类似于自己写了个代数系统,方便 debug 和找程序漏洞。具体来说就是要把 \(tag\) 和线段树节点 \(tr\) 之间的运算 \(+\) 重载一遍。
Code
#include<bits/stdc++.h>
using namespace std;
#define int long long
const int N=3e5+10;
int n;
int op[N],l[N],r[N];
int b[N<<2],cnt;
struct TAG
{
int f,as;
inline void reset(){f=0,as=-1;}
TAG operator +(const TAG &x)
{
if(as==-1&&x.as==-1)return {f^x.f,-1};
else if(as==-1&&x.as!=-1)return {0,x.as};
else if(as!=-1&&x.as==-1)return {f,as^x.f};
else if(as!=-1&&x.as!=-1)return {0,x.as};
}
}tag[N<<2];
struct sgt
{
int l,r,val;
sgt operator +(const sgt &x)
{
return {l,x.r,val+x.val};
}
sgt operator +(const TAG &x)
{
if(x.as==-1)return {l,r, x.f==1 ? r-l+1-val : val };
else
{
if(x.as==0) return {l,r,r-l+1};
else return {l,r,0};
}
}
#define L tr[x].l
#define R tr[x].r
}tr[N <<2];
inline void addtag(int x,TAG v){tr[x]=tr[x]+v,tag[x]=tag[x]+v;}
inline void pd(int x)
{
addtag(x<<1,tag[x]),addtag(x<<1|1,tag[x]);
tag[x].reset();
}
inline void build(int x,int l,int r)
{
tr[x].l=l,tr[x].r=r;
if(l==r)return ;
int mid=l+r>>1;
build(x<<1,l,mid),build(x<<1|1,mid+1,r);
}
inline void modify(int x,int l,int r,TAG v)
{
if(l<=L&&R<=r)return addtag(x,v);
int mid=L+R>>1;
pd(x);
if(l<=mid)modify(x<<1,l,r,v);
if(r>mid)modify(x<<1|1,l,r,v);
tr[x]=tr[x<<1]+tr[x<<1|1];
}
inline void dis(int x){cout<<"ID:"<<x<<' '<<tr[x].val<<'\n';}
inline int query(int x)
{
if(L==R)return L;
pd(x);
if(tr[x<<1].val)return query(x<<1);
else return query(x<<1|1);
}
inline void pre()
{
cin>>n;
b[++cnt]=1;
for(int i=1;i<=n;++i)
{
cin>>op[i]>>l[i]>>r[i];
b[++cnt]=l[i],b[++cnt]=r[i],b[++cnt]=r[i]+1;
}
sort(b+1,b+cnt+1);
cnt=unique(b+1,b+cnt+1)-b-1;
for(int i=1;i<=n;++i)
{
l[i]=lower_bound(b+1,b+cnt+1,l[i])-b;
r[i]=lower_bound(b+1,b+cnt+1,r[i])-b;
}
build(1,1,cnt);
modify(1,1,cnt,{0,0});
}
inline void solve()
{
pre();
for(int i=1;i<=n;++i)
{
if(op[i]==1)modify(1,l[i],r[i],{0,1});
else if(op[i]==2)modify(1,l[i],r[i],{0,0});
else modify(1,l[i],r[i],{1,-1});
cout<<b[query(1)]<<'\n';
}
}
signed main()
{
ios::sync_with_stdio(0);
cin.tie(0);
return solve(),0;
}
本文来自博客园,作者:Hanggoash,转载请注明原文链接:https://www.cnblogs.com/Hanggoash/p/19027179

浙公网安备 33010602011771号