P2617 Dynamic Rankings

前言

这是一道已经咕了很久的题。

第一次想写是因为 luan 讲了这道题。

直到她又出现在了我的智能推荐里。

我知道我不能再等了!

我要去和她表白!!

类题总结

关于各种第 k 大问题肯定已经见过很多了。

反正各种奇奇怪怪的解法都是有的。

比较主流的有:

静态整体第 k 大(小)

这玩意有两种解法。

首先是显而易见的 std::sort.

sort(a+1,a+1+n);
//reverse(a+1,a+1+n);
cout<<a[k]<<endl;

生怕你不会写这玩意

时间复杂度 $\text{O(nlogn)}$。

之后这玩意是存在 $\text{O(n)}$ 做法的。

考虑魔改快排。

每次选择基准值的时候记录一些大于基准值的个数 $cnt$ ,之后根据 $cnt$ 选择一半进入,就可以在平均线性之间内统计出第 k 大(小)。

int getk(int *v, int l, int r, int k){
    if(l<r){
        int i=l,j=l;
        for(;j<r;j++){
            if(v[j]<v[r]){
                swap(v[i++],v[j]);
            }
        }
        swap(v[i],v[r]);
        if(k==i)  return v[i];
        else if(k<i)  return getk(v,l,i-1,k);
        else return getk(v,i+1,r,k);
    }
    else return v[l];
}

动态整体第 k 大(小)

权值线段树,插入直接在树上插入,然后查询也直接查就行,像个桶一样。

值域小一点的话直接建树就行了,大一点就动态开点。

namespace SegmentTree{

    struct SegmentTree{
        int v;
        #define ls rt<<1
        #define rs rt<<1|1
    }st[maxn<<2]; 

    inline void push_up(int rt){
        st[rt].v=st[ls].v+st[rs].v;
    }

    void modify(int rt,int l,int r,int pos,int val){
        if(l==r){
            st[rt].v+=val;
            return;
        }

        int mid=(l+r)>>1;
        if(pos<=mid)modify(ls,l,mid,pos,val);
        else modify(rs,mid+1,r,pos,val);

        push_up(rt);
    }

    int ask(int rt,int l,int r,int val){//查询第 k 大
        if(l==r)return l;

        int mid=(l+r)>>1;
        if(val<=st[rs].v)ask(rs,mid+1,r,val);
        else ask(ls,l,mid,val-st[rs].v);
    }
}

复杂度 $\text{O(nlogn)}$

静态区间第 k 大(小)

也就是主席树板子题了。

具体看这个就行:p3834 可持久化线段树模板(主席树)

动态区间第 k 大(小)

就是本题了。

考虑修改操作。

由于常规的主席树是依靠前缀和的思想来求解区间第 k 大的,于是可以想到求解动态区间第 k 大首要问题是如何维护前缀和的问题。

对于维护前缀和,我们通常有如下几种方式:

暴力,树状数组,线段树。

于是有一个思路:加一个数据结构来维护前缀和。

加上去的数据结构放在什么位置呢?

由于主席树需要前缀和来作差,于是可以发现树状数组是要套在主席树外面的。

于是明白树状数组每个节点都维护一个线段树的根节点。

分开考虑修改以及查询操作。

修改操作

树状数组每次单点修改复杂度 $\text{O(logn)}$ ,对应 $\text{O(logn)}$ 棵权值线段树会受到影响,又因为每次修改需要对应到线段树内,线段树内每次又会有 $\text{O(logn)}$ 个节点需要修改。于是单次复杂度 $\text{O}(log^2n)$

每次修改需要用树状数组定位到需要修改的线段树的根节点位置,之后进行修改即可:

void modify(int &rt,int l,int r,int pos,int val){
    if(!rt)rt=++node;
    
    if(l==r){
        st[rt].v+=val;
        return;
    }

    int mid=(l+r)>>1;
    if(pos<=mid)modify(ls(rt),l,mid,pos,val);
    else modify(rs(rt),mid+1,r,pos,val);

    push_up(rt);
}

void update(int pos,int x,int val){
    for(re int i=pos;i<=n;i+=lowbit(i)){//处理涉及到的 log(n) 个根节点,就是对应的主席树
        modify(root[i],1,tmp,x,val);//线段树上修改log(n) 个节点
    }
}

int main() {
    ...

    for(re int i=1;i<=m;i++){
        if(q[i].op){//modify
            update(q[i].pos,a[q[i].pos],-1);//把原数删去
            a[q[i].pos]=q[i].val;//换成现在要的数
            update(q[i].pos,q[i].val,1);//在树上修改
        }
    }

}

查询操作

对于查询操作,依然是要用到前缀和。

由于使用了树状数组维护前缀和,于是我们直接在树状数组上取出对应的 $\text{O(logn)}$ 个根节点,也就是对应的主席树根节点,取的时候直接体现作差就行。

之后就和普通的主席树查询区间第 k 大一样,根据作差得到的值直接递归进入左子树或者右子树即可。

复杂度 $\text{O}(log^2n)$

int query(int l,int r,int k){
    if(l==r)return l;

    int mid=(l+r)>>1;
    int xx=0;
    for(re int i=1;i<=cnt1;i++)xx-=st[ls(ct1[i])].v;//作差得到cnt值
    for(re int i=1;i<=cnt2;i++)xx+=st[ls(ct2[i])].v;
    
    if(xx>=k){//左递归,连带着log(n)棵主席树一起进入左儿子
        for(re int i=1;i<=cnt1;i++)ct1[i]=ls(ct1[i]);
        for(re int i=1;i<=cnt2;i++)ct2[i]=ls(ct2[i]);
        
        return query(l,mid,k);
    }

    else {////右递归,连带着log(n)棵主席树一起进入右儿子
        for(re int i=1;i<=cnt1;i++)ct1[i]=rs(ct1[i]);
        for(re int i=1;i<=cnt2;i++)ct2[i]=rs(ct2[i]);
        
        return query(mid+1,r,k-xx);
    }
}

inline void located(int l,int r){//取出对应的log(n)个线段树的根节点。
    cnt1=cnt2=0;
    for(re int i=l-1;i;i-=lowbit(i))ct1[++cnt1]=root[i];//取l-1和r,用于作差
    for(re int i=r;i;i-=lowbit(i))ct2[++cnt2]=root[i];  
}

int main() {
    ...

    for(re int i=1;i<=m;i++){
        else {
            located(q[i].l,q[i].r);
            ans=b[query(1,tmp,q[i].k)];
            printf("%d\n",ans);
        }
    }

}

复杂度分析

查询操作 $\text{O}(log^2n)$,修改操作 $\text{O}(log^2n)$,总体复杂度 $\text{O}(mlog^2n)$

由于动态开点,所以空间复杂度就是对应的查询和修改所需要的空间。

于是 空间复杂度和时间复杂度可以认为是同阶的。

线段树大小开 $400$ 倍就够了。

具体实现的时候建议封一个 namespace ,看着也好看调着也爽。

似乎代码没有想象的那么长?

CODE:

//#define LawrenceSivan

#include <bits/stdc++.h>
using namespace std;

typedef long long ll;
typedef unsigned long long ull;
#define INF 0x3f3f3f3f
#define re register
const int maxn=1e5+5;

int n,m,ans;

char op;

int a[maxn],b[maxn<<1],tot,tmp;

int ct1[maxn],ct2[maxn],cnt1,cnt2;

struct QUERY{
    int l,r,k,op;
    int pos,val;
}q[maxn];

namespace SegmentTree{
    inline void Discretization(){
        sort(b+1,b+1+tot);
        tmp=unique(b+1,b+1+tot)-b-1;
        for(re int i=1;i<=n;i++)a[i]=lower_bound(b+1,b+1+tmp,a[i])-b;
        for(re int i=1;i<=m;i++){
            if(q[i].op==1)q[i].val=lower_bound(b+1,b+1+tmp,q[i].val)-b;
        }
    }

    struct SegmentTree{
        int lc,rc,v;
        #define ls(x) st[x].lc
        #define rs(x) st[x].rc
    }st[maxn*400];

    int node,root[maxn];

    inline void push_up(int rt){
        st[rt].v=st[ls(rt)].v+st[rs(rt)].v;
    }

    void modify(int &rt,int l,int r,int pos,int val){
        if(!rt)rt=++node;
        
        if(l==r){
            st[rt].v+=val;
            return;
        }

        int mid=(l+r)>>1;
        if(pos<=mid)modify(ls(rt),l,mid,pos,val);
        else modify(rs(rt),mid+1,r,pos,val);

        push_up(rt);
    }

    int query(int l,int r,int k){
        if(l==r)return l;

        int mid=(l+r)>>1;
        int xx=0;
        for(re int i=1;i<=cnt1;i++)xx-=st[ls(ct1[i])].v;
        for(re int i=1;i<=cnt2;i++)xx+=st[ls(ct2[i])].v;
        
        if(xx>=k){
            for(re int i=1;i<=cnt1;i++)ct1[i]=ls(ct1[i]);
            for(re int i=1;i<=cnt2;i++)ct2[i]=ls(ct2[i]);
            
            return query(l,mid,k);
        }

        else {
            for(re int i=1;i<=cnt1;i++)ct1[i]=rs(ct1[i]);
            for(re int i=1;i<=cnt2;i++)ct2[i]=rs(ct2[i]);
            
            return query(mid+1,r,k-xx);
        }
    }

}

using namespace SegmentTree;

namespace BIT{
    #define lowbit(x) (x&-x)

    void update(int pos,int x,int val){
        for(re int i=pos;i<=n;i+=lowbit(i)){
            modify(root[i],1,tmp,x,val);
        }
    }

    inline void located(int l,int r){
        cnt1=cnt2=0;
        for(re int i=l-1;i;i-=lowbit(i))ct1[++cnt1]=root[i];
        for(re int i=r;i;i-=lowbit(i))ct2[++cnt2]=root[i];  
    }

}

using namespace BIT;

template <typename T>
inline void read(T &x){
    T f=1;char ch=getchar();
    while (!isdigit(ch)) {if(ch=='-')f=-1;ch=getchar();}
    while (isdigit(ch)){x=x*10+(ch^48);ch=getchar();}
    x*=f;
}

int main() {
#ifdef LawrenceSivan
    freopen("aa.in", "r", stdin);
    freopen("aa.out", "w", stdout);
#endif
    read(n);read(m);
    for(re int i=1;i<=n;i++){
        read(a[i]);
        b[++tot]=a[i];
    }

    for(re int i=1;i<=m;i++){
        //cin>>op;
        scanf(" %c",&op);
        if(op=='C'){
            q[i].op=1,read(q[i].pos),read(q[i].val),b[++tot]=q[i].val;
        }
        else q[i].op=0,read(q[i].l),read(q[i].r),read(q[i].k);
    }

    Discretization();

    for(re int i=1;i<=n;i++){
        update(i,a[i],1);
    }

    for(re int i=1;i<=m;i++){
        if(q[i].op){
            update(q[i].pos,a[q[i].pos],-1);
            a[q[i].pos]=q[i].val;
            update(q[i].pos,q[i].val,1);
        }
        else {
            located(q[i].l,q[i].r);
            ans=b[query(1,tmp,q[i].k)];
            printf("%d\n",ans);
        }
    }



    return 0;
}