【JZOJ5710】【北大夏令营2018模拟5.13】Mex(主席树)

Problem

  给定长度为 n 的序列 a。现有 m 次询问,每次给定 l 和 r,询问区间 [l,r] 的数构成的集合的 mex 值。给定数据类型t,当t=1时强制在线。

Hint

这里写图片描述

Solution

  这道题有点思维难度,所以博主在此写下三种算法。

算法Ⅰ:线段树

  70points的数据允许离线,所以我们故意想一想离线。
  我们可以将询问按左端点排序,然后依次枚举每个位置作为左端点,预处理出每个位置作为右端点的mex值(即1~任意i的mex值),将其存在一棵线段树里。我们同时预处理出每个数的下一个与其相同的数的位置next[i]。
  设当前左端点为i,我们要将其右移一格,其实就相当于令i+1~next[i]-1中所有mex与a[i]取min。利用线段树区间修改即可。
  若我们要询问区间[l,r],则当左端点枚举到i时,直接查询线段树中r这个位置的值即可。
  时间复杂度: O((n+m)log2n) O ( ( n + m ) ∗ l o g 2 n ) 。期望得分:70。

Code
#include <cstdio>
#include <algorithm>
#include <vector>
#include <map>
#define A v<<1
#define B A|1
#define fi first
#define se second
#define mp make_pair
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;
typedef pair<int,int> P;

const int N=2e5+1,inf=0x7FFFFFFF;
int i,n,m,a[N],mex[N],next[N],l,r,tp;
map<int,int>last;
vector<P>q[N];
void scan()
{
    scanf("%d%d%d",&n,&m,&tp);
    fo(i,1,n)
    {
        scanf("%d",&a[i]);next[last[a[i]]]=i;last[a[i]]=i;
        mex[i]=mex[i-1];while(last[mex[i]])mex[i]++;
    }
    fo(i,1,n)if(!next[i])next[i]=n+1;
    fo(i,1,m)
    {
        scanf("%d%d",&l,&r);
        q[l].push_back(mp(i,r));
    }
}

int t[N<<2];
void maketree(int v,int l,int r)
{
    if(l==r){t[v]=mex[l];return;}
    t[v]=inf;int mid=l+r>>1;
    maketree(A,l    ,mid);
    maketree(B,mid+1,r  );
}

int x,y,ans[N],val;
vector<P>::iterator it;
inline void push(int v)
{
    if(t[v]==inf)return;
    t[A]=min(t[A],t[v]);
    t[B]=min(t[B],t[v]);
    t[v]=inf;
}
int query(int v,int l,int r)
{
    if(l==r)return t[v];
    push(v);
    int mid=l+r>>1;
    if(x<=mid)return query(A,l,mid);
    return query(B,mid+1,r);
}
void modify(int v,int l,int r)
{
    if(r<x||l>y)return;
    if(x<=l&&r<=y){t[v]=min(t[v],val);return;}
    int mid=l+r>>1;
    modify(A,l,mid);modify(B,mid+1,r);
}
void work()
{
    fo(l,1,n)
    {
        for(it=q[l].begin();it!=q[l].end();it++)
        {
            P t=*it;i=t.fi;x=t.se;
            ans[i]=query(1,1,n);
        }

        x=l+1;y=next[l]-1;val=a[l];
        modify(1,1,n);
    }
}

void print()
{
    fo(i,1,n)printf("%d\n",ans[i]);
}

int main()
{
    scan();
    maketree(1,1,n);
    work();
    print();
}
算法Ⅱ:主席树

  算法Ⅰ其实可以轻松改为在线:使用主席树。
  但这区别于一般的主席树:一般的主席树是对于每个位置i都建一棵存储1~i信息的权值线段树;本做法则是对于每个左端点i都建一棵存储当右端点为i~n时的mex值。这就相当于用主席树做可回溯至历史版本的区间修改、查询一样。
  时间复杂度: O((n+m)log2n) O ( ( n + m ) ∗ l o g 2 n ) 。期望得分:100。

Code
#include <cstdio>
#include <algorithm>
#include <vector>
#include <map>
#define A t[v].l
#define B t[v].r
#define fi first
#define se second
#define mp make_pair
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;
typedef pair<int,int> P;

const int N=2e5+1,inf=0x7FFFFFFF;
int i,n,m,tp,a[N],mex[N],next[N],l,r;
map<int,int>last;
void scan()
{
    scanf("%d%d%d",&n,&m,&tp);
    fo(i,1,n)
    {
        scanf("%d",&a[i]);next[last[a[i]]]=i;last[a[i]]=i;
        mex[i]=mex[i-1];while(last[mex[i]])mex[i]++;
    }
    fo(i,1,n)if(!next[i])next[i]=n+1;
}

int x,y,rt[N],cnt,val;
struct node
{
    int l,r,v;
}t[72*N];
void partition(int v,int l,int r)
{
    if(l==r){t[v].v=mex[l];return;}
    t[v].v=inf;int mid=l+r>>1;
    partition(A=++cnt,l    ,mid);
    partition(B=++cnt,mid+1,r  );
}
void modify(int&v,int l,int r)
{
    if(r<x||l>y)return;
    t[++cnt]=t[v],v=cnt;
    if(x<=l&&r<=y){t[v].v=min(t[v].v,val);return;}
    int mid=l+r>>1;
    modify(A,l,mid);modify(B,mid+1,r);
}
void maketree()
{
    partition(rt[1]=cnt=1,1,n);
    fo(i,1,n-1)
    {
        x=i+1;y=next[i]-1;val=a[i];
        modify(rt[i+1]=rt[i],1,n);
    }
}

int ans;
void query(int v,int l,int r)
{
    ans=min(ans,t[v].v);
    if(l==r)return;
    int mid=l+r>>1;
    if(x<=mid)
            query(A,l    ,mid);
    else    query(B,mid+1,r  );
}
void work()
{
    fo(i,1,m)
    {
        scanf("%d%d",&l,&r);if(tp)l^=ans,r^=ans;
        x=r;ans=inf;query(rt[l],1,n);
        printf("%d\n",ans);
    }
}

int main()
{
    freopen("mex.in","r",stdin);
    freopen("mex.out","w",stdout);
    scan();
    maketree();
    work();
}
算法Ⅲ:主席树

  不难发现,算法Ⅱ即不直观又比较复杂,这里讲一种比较简单的做法。
  对于每个右端点i,我们都可以建一棵线段树,记录1~i中每个数最后一次出现的位置。比如a={112,111,112,34,5135},当i=3时,线段树的第111个位置为2(a[2]=111),第112个位置为3(a[3]=112)。然后,这棵线段树是存储min值的(比如上例中表示区间[111..112]的线段树的节点储存的值为min(2,3)=2,而表示区间[109..112]的点存储的值则为min(0,2)=0)。这样存储原因之后再说。
  对于每个询问[l,r],我们在第r棵线段树上查找。设当前节点为v,其左右儿子分别为A、B,线段树为t,若t[A]<l 则走左边;否则走右边。因为若t[A]<l,则说明左半区间中至少有一个数最后一次出现在l之前,亦即左半区间中至少有一个数在[l,r]区间中没有出现;而我们又要mex值尽量小。
  时间复杂度: O((n+m)log2ai) O ( ( n + m ) ∗ l o g 2 a i ) 。期望得分:100。

Code
#include <cstdio>
#define A t[v].l
#define B t[v].r
#define min(a,b) ((a)<(b)?(a):(b))
#define fo(i,a,b) for(i=a;i<=b;i++)
using namespace std;

const int N=2e5+1,Mx=1e9;
int i,n,m,tp,a,rt[N],cnt;
struct node
{
    int v,l,r;
}t[N*32];
void modify(int&v,int l,int r)
{
    t[++cnt]=t[v];v=cnt;
    if(l==r){t[v].v=i;return;}
    int mid=l+r>>1;
    a<=mid?modify(A,l,mid):modify(B,mid+1,r);
    t[v].v=min(t[A].v,t[B].v);
}
void maketree()
{
    scanf("%d%d%d",&n,&m,&tp);
    fo(i,1,n)
    {
        scanf("%d",&a);
        modify(rt[i]=rt[i-1],0,Mx);
    }
}

int l,r,x,ans;
void query(int v,int l,int r)
{
    if(!A){ans=l;return;}
    int mid=l+r>>1;
    t[A].v<x?query(A,l,mid):query(B,mid+1,r);
}
void work()
{
    fo(i,1,m)
    {
        scanf("%d%d",&l,&r);if(tp)l^=ans,r^=ans;
        x=l;query(rt[r],0,Mx);printf("%d\n",ans);
    }
}

int main()
{
    freopen("mex.in","r",stdin);
    freopen("mex.out","w",stdout);
    maketree();
    work();
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值