主席树 —— ③动态区间第K小(树套树,带修改主席树)

动态区间第K小

动态区间第K小,又称带修改主席树(动态主席树),对于初学真的不太友好。因为这个和静态区间第K小(静态主席树)数据结构都完全不同了。

和求静态第K小一样,我们要得到[L, R]状态的线段树,才能进行二分得到第K小。

静态主席树 是一个 可持久化线段树,每次利用 前缀和来计算[L, R]状态,但是前缀和就不便于进行修改操作了,那么对于可修改的区间求和,我们就要用到 树状数组/线段树

这里就是要采用 树套树->
外层树状数组/线段树:即区间线段树,每一个结点都存储 当前位置区间一棵主席树(即主席树的根结点)
内层主席树:即权值线段树,每个结点存储 当前权值区间出现次数

所以外层主要用来 锁定位置(就是树状数组/线段树求区间和的过程),由于这里的求区间和每个元素是主席树,不能直接相加,那么就要保存所有求得[L,R]状态需要的主席树(的根结点),然后同时遍历,对应的点求和,求得[L, R]的状态。

具体可以看这篇blog(写的真的好):https://www.cnblogs.com/LiuRunky/p/Sustainable_Segment_Tree.html

光看文字说明其实挺难理解的(这东西真的很难用文字说清),看了代码会更好理解。
但由于个人写惯了线段树,对树状数组不是很熟悉,导致看代码理解的过程十分坎坷(找到全是树状数组+主席树)
于是这里打算把两种形式都写一下。(不过还是推荐树状数组,空间小,易编写,无需递归)

此外,注意离散化不仅要对原序列,对修改的值也要一同离散化,所以要先把所有修改操作先读入,存储起来,离散化以后再开始各条操作。修改时,要先令原值出现次数-1,再令新值出现次数+1。



模板题:LUOGU - P2617 Dynamic Rankings


①线段树套主席树

时限2s,但是40%的点超时了0.2s左右,在尝试各种卡常优化的排列组合后依旧有15%的点TLE(有些卡常甚至更慢了,这东西也太玄学了)

综合考虑可能是外层用线段树,更新和定位时递归调用时间产生的时间消耗(树状数组就不需要递归了),也有可能是测试数据正好对这种写法不友好吧…

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=1e5+50;
int n,m,a[maxn];
int root[maxn<<2],t[maxn*400],ls[maxn*400],rs[maxn*400],cnt=0;
int b[2*maxn],N=0;
struct
{
    int opt,i,j,k;
}q[maxn];
void init()
{
    sort(b+1,b+N+1);
    N=unique(b+1,b+N+1)-(b+1);
    for(int i=1;i<=n;i++)
        a[i]=lower_bound(b+1,b+N+1,a[i])-b;
    for(int i=1;i<=m;i++)
    {
        if(q[i].opt)
            q[i].k=lower_bound(b+1,b+N+1,q[i].k)-b;
    }
}
void updata_in(int &rt,int l,int r,int x,int val)
{                              //内层更新,对应权值出现次数+1/-1
    if(!rt)
        rt=++cnt;
    if(l==r)
    {
        t[rt]+=val;
        return;
    }
    int mid=(l+r)>>1;
    if(x<=mid)
        updata_in(ls[rt],l,mid,x,val);
    else
        updata_in(rs[rt],mid+1,r,x,val);
    t[rt]=t[ls[rt]]+t[rs[rt]];
}
void updata_out(int rt,int l,int r,int pos,int x,int val)
{                                       //外层更新,找到logN个插入位置
    updata_in(root[rt],1,N,x,val);
    if(l==r)
        return;
    int mid=(l+r)>>1;
    if(pos<=mid)
        updata_out(rt<<1,l,mid,pos,x,val);
    else
        updata_out(rt<<1|1,mid+1,r,pos,x,val);
}
int RT[maxn],cntr;   //存储待相加的主席树(的根结点)
void locate(int rt,int l,int r,int ql,int qr)
{                       //根据线段树,锁定求得[L,R]区间的线段树(各个点对应求和)需要哪些主席树
    if(ql<=l&&r<=qr)    //找到对应区间的主席树(待相加)
    {
        RT[++cntr]=root[rt];
        return;
    }
    if(r<ql||l>qr)
        return;
    int mid=(l+r)>>1;
    locate(rt<<1,l,mid,ql,qr);
    locate(rt<<1|1,mid+1,r,ql,qr);
}
int query(int l,int r,int k)
{
    if(l==r)
      return l;
    int mid=(l+r)>>1;
    int suml=0;
    for(int i=1;i<=cntr;i++)   //计算左子树的sum
        suml+=t[ls[RT[i]]];
    if(suml>=k)
    {
        for(int i=1;i<=cntr;i++)  //根结点也要一同更新
            RT[i]=ls[RT[i]];
        return query(l,mid,k);
    }
    else
    {
        for(int i=1;i<=cntr;i++)  //根结点也要一同更新
            RT[i]=rs[RT[i]];
        return query(mid+1,r,k-suml);
    }
}
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        b[++N]=a[i];
    }
    char opt[5];
    for(int i=1;i<=m;i++)
    {
        scanf("%s",opt);
        if(opt[0]=='Q')
        {
            q[i].opt=0;
            scanf("%d",&q[i].i);
            scanf("%d",&q[i].j);
            scanf("%d",&q[i].k);
        }
        else
        {
            q[i].opt=1;
            scanf("%d",&q[i].i);
            scanf("%d",&q[i].k);
            b[++N]=q[i].k;
        }
    }
    init();   //离散化
    for(int i=1;i<=n;i++)
        updata_out(1,1,n,i,a[i],1);
    for(int i=1;i<=m;i++)
    {
        if(q[i].opt)
        {
            updata_out(1,1,n,q[i].i,a[q[i].i],-1);  //原值出现次数-1
            a[q[i].i]=q[i].k;
            updata_out(1,1,n,q[i].i,q[i].k,1);      //新值出现次数+1
        }
        else
        {
            cntr=0;
            locate(1,1,n,q[i].i,q[i].j);          //定位
            printf("%d\n",b[query(1,N,q[i].k)]);  //查询
        }
    }
    return 0;
}


②树状数组套主席树

树状数组空间消耗为O(N),更节省空间,而且查询和单点更新用循环就可以实现,速度相对于递归调用更快些,编写起来码量也小,所以动态主席树基本都是该形式。

因为树状树状求和只能求得前缀和(1~i),所以要分别求sum[1, L-1] 和sum[1, R]的然后相减,得到区间和。

#include<bits/stdc++.h>
#define LL long long
using namespace std;
const int INF=0x3f3f3f3f;
const int maxn=1e5+50;
int n,m,a[maxn];
int root[maxn],t[maxn*400],ls[maxn*400],rs[maxn*400],cnt=0;
int b[maxn*2],N=0;
struct
{
    int opt,i,j,k;
}q[maxn];
void init()
{
    sort(b+1,b+N+1);
    N=unique(b+1,b+N+1)-(b+1);
    for(int i=1;i<=n;++i)
        a[i]=lower_bound(b+1,b+N+1,a[i])-b;
    for(int i=1;i<=m;++i)
    {
        if(q[i].opt)
            q[i].k=lower_bound(b+1,b+N+1,q[i].k)-b;
    }
}
int lowbit(int x)
{
    return x&(-x);
}
void updata_in(int &rt,int l,int r,int x,int val)
{                              //内层更新,对应权值出现次数+1/-1
    if(!rt)
        rt=++cnt;
    if(l==r)
    {
        t[rt]+=val;
        return;
    }
    int mid=(l+r)>>1;
    if(x<=mid)
        updata_in(ls[rt],l,mid,x,val);
    else
        updata_in(rs[rt],mid+1,r,x,val);
    t[rt]=t[ls[rt]]+t[rs[rt]];
}
void updata_out(int pos,int x,int val)
{                              //外层更新,找到logN个插入位置
    for(int i=pos;i<=n;i+=lowbit(i))
        updata_in(root[i],1,N,x,val);
}
int RT1[maxn],RT2[maxn],cnt1,cnt2;
void locate(int ql,int qr)    //找到对应区间的主席树
{
    cnt1=cnt2=0;
    for(int i=ql-1;i>0;i-=lowbit(i))  //求和[1,L-1]需要的主席树
        RT1[++cnt1]=root[i];
    for(int i=qr;i>0;i-=lowbit(i))    //求和[1,R]需要的主席树
        RT2[++cnt2]=root[i];
}
int query(int l,int r,int k)
{
    if(l==r)
      return l;
    int mid=(l+r)>>1;
    int suml=0;       //计算左子树的sum
    for(int i=1;i<=cnt1;++i)   //减去左子树sum[1,L-1]
        suml-=t[ls[RT1[i]]];
    for(int i=1;i<=cnt2;++i)   //加上左子树sum[1,R]
        suml+=t[ls[RT2[i]]];
    if(suml>=k)
    {
        for(int i=1;i<=cnt1;++i)  //根结点也要一同更新
            RT1[i]=ls[RT1[i]];
        for(int i=1;i<=cnt2;++i)
            RT2[i]=ls[RT2[i]];
        return query(l,mid,k);
    }
    else
    {
        for(int i=1;i<=cnt1;++i)   //根结点也要一同更新
            RT1[i]=rs[RT1[i]];
        for(int i=1;i<=cnt2;++i)
            RT2[i]=rs[RT2[i]];
        return query(mid+1,r,k-suml);
    }
}
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=n;i++)
    {
        scanf("%d",&a[i]);
        b[++N]=a[i];
    }
    char opt[5];
    for(int i=1;i<=m;i++)
    {
        scanf("%s",opt);
        if(opt[0]=='Q')
        {
            q[i].opt=0;
            scanf("%d",&q[i].i);
            scanf("%d",&q[i].j);
            scanf("%d",&q[i].k);
        }
        else
        {
            q[i].opt=1;
            scanf("%d",&q[i].i);
            scanf("%d",&q[i].k);
            b[++N]=q[i].k;
        }
    }
    init();    //离散化
    for(int i=1;i<=n;++i)
        updata_out(i,a[i],1);
    for(int i=1;i<=m;++i)
    {
        if(q[i].opt)
        {
            updata_out(q[i].i,a[q[i].i],-1);  //原值出现次数-1
            a[q[i].i]=q[i].k;
            updata_out(q[i].i,q[i].k,1);      //新值出现次数+1
        }
        else
        {
            locate(q[i].i,q[i].j);                //定位
            printf("%d\n",b[query(1,N,q[i].k)]);  //查询
        }
    }
    return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值