线段树与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;
}
posted @ 2025-08-07 17:12  Hanggoash  阅读(28)  评论(0)    收藏  举报
动态线条
动态线条end