区间第k小算法学习笔记

前置知识:值域线段树,可持续化线段树,树状数组

动态整体第k小

题目:给定一个序列和m次操作,每次操作修改单点或者询问整个序列第k小的数

首先考虑暴力,对于每次修改都直接排序的话,复杂度为O(nmlogn),也可以魔改一下排序方法,不过一般的暴力还是没办法过

整体第k小带修改很明显可以用平衡树做,不过编程较麻烦(而且大材小用),所以不考虑

值域线段树

值域线段树可以很方便的O(logn)查询一次所有数中比某个值小的数的个数,于是我们可以考虑用它解决这一类问题,当然一般来说值域线段树是和离散化配套使用的

做法:

将所有数离散化后加入值域线段树,修改操作就直接删除旧的,加入新的

对于查询操作,从根节点开始,当前节点的左儿子保存着\(≤\)mid的数的个数sum,如果sum\(≥\)k,就说明第k小应该在左边,递归到左儿子,否则k-=sum,递归给右儿子(k-=sum是因为在整个区间找第k小等价于在右区间找第k-sum小)

时间复杂度为O(nlogn),空间复杂度O(n*4)


静态前缀第k小

题目:每次查询前x个数中的第k小,无修改

做法:这里改变一下上面的方法。上面的做法中,可以发现,sum的大小表示的是所有数\(≤\)mid的数的个数,而这里是要求前x个数\(≤\)mid的数的个数,于是我们需要对每一个数a[i]加入之后都对前i个数建立一颗值域线段树,询问前x个数的时候就使用第x个线段树

可持续化线段树

显然不可能真的建立n个值域线段树

链接

时空复杂度O(nlogn)

静态区间第k小

题目:查询改为[ l , r ]区间,无修改

首先明确一件事,对于上面建的n个值域线段树(假装把n个树都单独拆出来),形态完全相同,并且对于每一个树的相同位置,意义几乎一样,比如,第x个树和第y个树的某个位置都表示不大于c的数的个数,只不过一个是针对前a[1~x],另一个a[1~y]。所以可以考虑前缀和的思想,假设y \(>\) x,用y树一个节点减去x树上对应节点就可以表示a[ x+1 ~ y ]这一段上不大于c的数

做法:

对于查询[ l , r ],同时使用l-1和r两个值域线段树,每次的sum由r树的左儿子减去l-1树的左儿子得到,向下递归时两个根要一起向同一个方向走

时空复杂度O(nlogn)

Code:

#include<bits/stdc++.h>
#define N 200005
using namespace std;
int n,m;
int ref[N],len;
int a[N],ndsum;
int root[N],ls[N*20],rs[N*20],sum[N*20];

template <class T>
void read(T &x)
{
    char c;int sign=1;
    while((c=getchar())>'9'||c<'0') if(c=='-') sign=-1; x=c-48;
    while((c=getchar())>='0'&&c<='9') x=(x<<1)+(x<<3)+c-48; x*=sign;
}

void build(int &rt,int l,int r)
{
    rt=++ndsum;
    if(l==r) return;
    int mid=(l+r)>>1;
    build(ls[rt],l,mid);
    build(rs[rt],mid+1,r);
}
void copynode(int x,int y)
{
    ls[x]=ls[y];
    rs[x]=rs[y];
    sum[x]=sum[y]+1;//复制的链上都会增加 1 
}
int modify(int rt,int l,int r,int x,int val)
{
    int t=++ndsum;
    copynode(t,rt);
    if(l==r) return t;
    int mid=(l+r)>>1;
    if(mid>=x) ls[t]=modify(ls[rt],l,mid,x);
    else rs[t]=modify(rs[rt],mid+1,r,x);
    return t;
}
int query(int rt1,int rt2,int l,int r,int k)
{
    if(l==r) return l;
    int x=sum[ls[rt2]]-sum[ls[rt1]];
    int mid=(l+r)>>1;
    if(x>=k) return query(ls[rt1],ls[rt2],l,mid,k);
    else return query(rs[rt1],rs[rt2],mid+1,r,k-x);
}

int main()
{
    read(n);read(m);
    for(int i=1;i<=n;++i) read(a[i]),ref[++len]=a[i];
    sort(ref+1,ref+len+1);
    len=unique(ref+1,ref+len+1)-ref-1;
    build(root[0],1,len);//先建立一个空树 
    for(int i=1;i<=n;++i)
    {
        int t=lower_bound(ref+1,ref+len+1,a[i])-ref;//找到要加入的a[i]在ref中对应的下标 
        root[i]=modify(root[i-1],1,len,t);
    }
    for(int i=1;i<=m;++i)
    {
        int x,y,k;
        read(x);read(y);read(k);
        printf("%d\n",ref[query(root[x-1],root[y],1,len,k)]);
    }
    return 0;
}

动态区间第k小

对于上面的静态区间第k小问题,我们用类似前缀和的思想知道了区间[ l , r ]中不大于c的数的个数,于是对于带修改的前缀和问题,我们很容易想到用树状数组维护

这里采用类比法来方便理解

在树状数组中,查询前x个数的和的时候,我们将x二进制拆分使得遍历的点只有logn个,修改同理,这相当于将修改和查询的时间平均分到logn(暴力维护前缀和是查询O(1),修改O(n))。类似的,原来方法中询问的单纯从第x个树向下遍历(类比于直接输出sum[x])就变成了从logn颗树同时向下遍历,这样就可以使得修改操作变成修改logn颗树,将复杂度平均分给修改和查询。

Code:

#include<bits/stdc++.h>
#define N 100005
#define lowbit(x) ((x)&(-x))
using namespace std;
int n,m;
int a[N];
int exc[N<<1],len;//离散化数组 
int ls[N*400],rs[N*400],val[N*400],root[N<<1],sum;//值域线段树 
int son[2][100],s[2];//树状数组思路中应该处理的logn颗树(分为l,r) 

struct Order
{
    char o;
    int l,r,pos,val;
}order[N];

template <class T>
void read(T &x)
{
    char c;int sign=1;
    while((c=getchar())>'9'||c<'0') if(c=='-') sign=-1; x=c-48;
    while((c=getchar())>='0'&&c<='9') x=(x<<1)+(x<<3)+c-48; x*=sign;
}

void modify(int &rt,int l,int r,int pos,int v)//修改某一颗树 
{
    if(!rt) rt=++sum;
    val[rt]+=v;
    if(l==r) return;
    int mid=(l+r)>>1;
    if(mid>=pos) modify(ls[rt],l,mid,pos,v);
    else modify(rs[rt],mid+1,r,pos,v); 
}
int pre_modify(int pos,int v)//修改logn颗树 
{
    int k=lower_bound(exc+1,exc+len+1,a[pos])-exc;
    for(int i=pos;i<=n;i+=lowbit(i)) modify(root[i],1,len,k,v);
}
int query(int l,int r,int k)//询问 
{
    if(l==r) return l;
    int sum=0,mid=(l+r)>>1;
    for(int i=1;i<=s[1];++i) sum+=val[ls[son[1][i]]];//叠加每一颗树 (r)
    for(int i=1;i<=s[0];++i) sum-=val[ls[son[0][i]]];//减去每一颗树 (l) 
    if(k<=sum)//向左转,要处理的logn颗树都要转弯 
    {
        for(int i=1;i<=s[1];++i) son[1][i]=ls[son[1][i]];
        for(int i=1;i<=s[0];++i) son[0][i]=ls[son[0][i]];
        return query(l,mid,k);
    }
    else
    {
        for(int i=1;i<=s[1];++i) son[1][i]=rs[son[1][i]];
        for(int i=1;i<=s[0];++i) son[0][i]=rs[son[0][i]];
        return query(mid+1,r,k-sum);
    }
}

int main()
{
    read(n);read(m);
    for(int i=1;i<=n;++i) read(a[i]),exc[++len]=a[i];
    for(int i=1;i<=m;++i)
    {
        while((order[i].o=getchar())<40);
        if(order[i].o=='Q') {read(order[i].l);read(order[i].r);read(order[i].val);}
        if(order[i].o=='C') {read(order[i].pos);read(order[i].val);}
        exc[++len]=order[i].val;
    }
    sort(exc+1,exc+len+1);
    len=unique(exc+1,exc+len+1)-exc-1;
    for(int i=1;i<=n;++i) pre_modify(i,1);//加入a[i],建树 
    for(int opt=1;opt<=m;++opt)
    {
        if(order[opt].o=='Q')//询问 
        {
            s[1]=s[0]=0;
            for(int i=order[opt].r;i;i-=lowbit(i)) son[1][++s[1]]=root[i];
            for(int i=order[opt].l-1;i;i-=lowbit(i)) son[0][++s[0]]=root[i];
            //要同时处理的logn颗树,l,r 
            printf("%d\n",exc[query(1,len,order[opt].val)]);
        }
        else
        {
            pre_modify(order[opt].pos,-1);//删除目标 
            a[order[opt].pos]=order[opt].val;//修改 
            pre_modify(order[opt].pos,1);//加回去 
        }
    }
    return 0;
}

转载于:https://www.cnblogs.com/Chtholly/p/10740533.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值