离线区间Mex问题的O(nlogn)算法

问题描述

区间Mex问题,简单说来,就是要查询区间最小未出现正整数(非负整数和正整数可以通过整体加减一相互转换,这里不作区分了)。

详见 Rmq Problem / mex

离线区间Mex问题,用莫队算法 O ( n n ) O(n\sqrt n) O(nn ) 的求法已经很成型了;不过有一个离线情况下更好的方法。


我们首先考虑数字上限不是很大,不需要离散化的情况:

首先离线情况下,可以把查询按照右端点 R R R 排序。

之后可以随着右端点增加,逐个添加数据。我们的问题就变成了:对于当前的状态,所有数最后一次出现的位置中,比我所查询的左端点 L L L 小的最小的数字。

因此,我们考虑维护一棵 权值线段树,维护每个数字最后一次出现的位置,通过线段树维护位置的最小值。

查询时,给出 L L L ,如果 L L L 小于等整棵树的最小值,证明所有编号都出现过;之后在线段树上深搜,如果 L L L 大等于左子树的最小值,就去往左子树,否则去往右子树,直到到达叶子节点,这个节点代表的数就是我要找的最小未出现的数。


之后是 离散化 的处理办法:

处理办法是,在离散化的过程中,加入每个数+1 。那么效果就是,原来有两个不挨着的数因为离散化而挨在一起了;处理后他们就不会挨在一起,并且他们之间会隔着一个永远不会在序列中出现的数字。

仔细一想,你就会发现离散化的问题解决了。现在查到的结果作为排名找到原来的数字,结果就是正确的。


用到的算法和数据结构知识都比较基础(甚至比莫队还基础),整个过程中不论是排序、离散化、还是线段树,复杂度都是 O ( n l o g n ) O(nlogn) O(nlogn) ,因此总的复杂度也是 O ( n l o g n ) O(nlogn) O(nlogn)

附代码,如有错误,恳请指正:

#include <iostream>
#include <algorithm>
using namespace std;

const int maxTreeNode=1000000, maxn=200001;
class segmentTree{
public:
    segmentTree(const int limit):
        limit(limit), root(build(1, limit)){}
    int min()
    {
        return mi[root];
    }
    int ask(int x)
    {
        return ask(root, 1, limit, x);
    }
    void update(int d, int v)
    {
        update(root, 1, limit, d, v);
        return;
    }
private:
    int mi[maxTreeNode], son[2][maxTreeNode], totPoint;
    const int limit, root;
    int build(int l, int r)
    {
        int cur=++totPoint;
        if(l==r) return cur;
        int mid=l+r>>1;
        son[0][cur]=build(l, mid);
        son[1][cur]=build(mid+1, r);
        return cur;
    }
    void update(int k, int l, int r, int d, int v)
    {
        if(l==r){
            mi[k]=v;
            return;
        }
        int mid=l+r>>1;
        if(d<=mid) update(son[0][k], l, mid, d, v);
        else update(son[1][k], mid+1, r, d, v);
        mi[k]=std::min(mi[son[0][k]], mi[son[1][k]]);
        return;
    }
    int ask(int k, int l, int r, int x)
    {
        if(l==r) return l;
        int mid=l+r>>1;
        if(mi[son[0][k]]<x) return ask(son[0][k], l, mid, x);
        else return ask(son[1][k], mid+1, r, x);
    }
}; //线段树

struct query{
    int l, r, id;
    bool operator<(const query& another)const {
        return r==another.r ? l<another.l : r<another.r;
    }
}q[maxn]; //离线

int a[maxn], ans[maxn], cpy[maxn<<1], cnt;
int getRnk(int x)
{
    return lower_bound(cpy+1, cpy+cnt+1, x)-cpy;
}
int getNum(int k)
{
    return cpy[k];
}
int main()
{
    //freopen("test.in", "r", stdin);
    
    int n, m, l;
    cin >> n >> m;
    cpy[++cnt]=1; //放入1,作为下界
    
    for(int i=1; i<=n; i++)
        cin >> a[i], cpy[++cnt]=a[i], cpy[++cnt]=a[i]+1;
    for(int i=1; i<=m; i++)
        cin>> q[i].l >> q[i].r, q[i].id=i;
    
    sort(q+1, q+m+1);
    sort(cpy+1, cpy+cnt+1);
    cnt = unique(cpy+1, cpy+cnt+1)-cpy; //去重

    for(int i=1; i<=n; i++)
        a[i]=getRnk(a[i]); //离散化,用排名代替数据
    segmentTree* Tree=new segmentTree(cnt);

    int cur=0;
    for(int i=1; i<=m; i++){
        while(cur<q[i].r)
            Tree->update(a[cur], ++cur);
        ans[q[i].id]=Tree->ask(q[i].l);
    }
    
    for(int i=1; i<=m; i++)
        cout<<getNum(ans[i])<<endl;
    
    return 0;
}

update:

根据队友的指正,实际上不需要把每个数字+1都放进离散化序列之中,因为有可能成为mex的、原序列中没有出现过的数,只可能是把原序列排序从1开始连续数,得到的最小的未出现的数字。

因此对离散化部分做出如下更新:

for(int i=1; i<=n; i++)
    cin >> a[i], cpy[++cnt]=a[i], exist[a[i]]=true; //n是1e5量级的,所以最小未出现的数字也在1e5以内。
int index=0;
while(exist[index]) index++;
cpy[++cnt]=index;

如有错误,恳请指正。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值