动态区间第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;
}

&spm=1001.2101.3001.5002&articleId=97623026&d=1&t=3&u=f54cd99ac6b64e38a5364dbc1c43fd99)
2055

被折叠的 条评论
为什么被折叠?



