模板,动态区间kkk小:
给定一个长度为nnn的数组,mmm次操作,每次操作只可能是下面其中之一:
(1)给定l,r,kl,r,kl,r,k,求区间[l,r][l,r][l,r]的kkk小元素
(2)给定x,yx,yx,y,把a[x]a[x]a[x]改为yyy
树状数组套主席树解法:
让我们回顾一下区间静态区间kkk小的方法,利用主席树可以融合多颗线段树的性质,保存iii的前缀权值线段树,做差获得[l,r][l,r][l,r]的权值线段树,直接在树上二分。显然,iii的前缀权值线段树仅需要在i−1i-1i−1的前缀权值线段树上修改即可得到。总而言之,获得[l,r][l,r][l,r]的权值线段树,再在树上二分就是本质思想。
不妨在上面的思路上继续思考,我们把a[x]a[x]a[x]改成yyy,那么只需要在[x,n][x,n][x,n]的前缀上都修改一遍即可达成目的,可是这样的时间复杂度是O(nmlogn)O(nmlogn)O(nmlogn)的,mmm是修改次数,太慢
再进一步,我们能不能加速这个修改过程?由于是前缀线段树,并且存在区间求和,于是我们考虑树状数组,令树状数组每个点都是一个线段树,即主席树上的一个根,如果我们需要修改xxx位置的,能将原来单次修改的时间复杂度由O(nlogn)O(nlogn)O(nlogn)优化为O(log2n)O(log^2n)O(log2n)。那考虑如何获得差值权值线段树,在树状数组上,我们的区间求和策略是获得他们的前缀和做差,但由于树状数组存的是根,没有直接的前缀和,于是,每个根我们只存放一些链,也就是部分的线段树,当我们求得前缀和后,所有的链被合并在一起,成为一颗完整的线段树,这样就可以做差获得差值的权值线段树了
由于我们存放的是根,我们在合并后的线段树上二分,合并之后的线段树不好保存,不妨直接在部分树上分别二分,权值取所有对应点的总和,因此需要一个数组来保存他们的根,并且只有根满足lowbitlowbitlowbit规则(因为只有根在树状数组上),所以预先处理处根的数组,然后树上二分时所有点同时向左/右子树移动。在线段树上二分的时间复杂度时O(logn)O(logn)O(logn),同时二分O(logn)O(logn)O(logn)条,于是单词查询时间复杂度是O(log2n)O(log^{2}n)O(log2n),总时间复杂度是O(nlog2n)O(nlog^{2}n)O(nlog2n)。每次操作在一个根上最多插入一条长O(logn)O(logn)O(logn)的链,每次操作修改了O(logn)O(logn)O(logn)个根,于是总空间复杂度是O(nlog2n)O(nlog^{2}n)O(nlog2n)
#include<bits/stdc++.h>
#define ll long long
#define endl '\n'
using namespace std;
int n,m;
struct question
{
string s;
int l,r,k;
}q[100005];
int a[100005],vv[200005];
int tree[40000005],cnt,version[100005];
int lson[40000005],rson[40000005],vp,len;
inline int lowbit(int x){return -x&x;}
inline int& ls(int x){return lson[x];}
inline int& rs(int x){return rson[x];}
inline void push_up(int x){tree[x]=tree[ls(x)]+tree[rs(x)];}
void update(int nl,int l,int r,int x,int k)
{
if(l==r){tree[x]+=k;return;}
int mid=l+r>>1;
if(nl<=mid)
{
if(!ls(x)) ls(x)=++cnt;
update(nl,l,mid,ls(x),k);
}
else
{
if(!rs(x)) rs(x)=++cnt;
update(nl,mid+1,r,rs(x),k);
}
push_up(x);
}
void add(int x,int k,int v)
{
while(x<=n)
{
if(!version[x]) version[x]=++cnt;
update(k,1,len,version[x],v);
x+=lowbit(x);
}
}
int tmp1[100005],pp1,tmp2[100005],pp2;
int solve(int l,int r,int x,int k)
{
// printf("x=%d\n",x);
if(l==r) return l;
int mid=l+r>>1,sum=0;
for(int i=1;i<=pp1;i++) sum-=tree[ls(tmp1[i])];
for(int i=1;i<=pp2;i++) sum+=tree[ls(tmp2[i])];
if(k<=sum)
{
for(int i=1;i<=pp1;i++) tmp1[i]=ls(tmp1[i]);
for(int i=1;i<=pp2;i++) tmp2[i]=ls(tmp2[i]);
return solve(l,mid,ls(x),k);
}
else
{
for(int i=1;i<=pp1;i++) tmp1[i]=rs(tmp1[i]);
for(int i=1;i<=pp2;i++) tmp2[i]=rs(tmp2[i]);
return solve(mid+1,r,rs(x),k-sum);
}
}
int solve(int l,int r,int k)
{
pp1=pp2=0;
for(int i=l-1;i;i-=lowbit(i)) tmp1[++pp1]=version[i];
for(int i=r;i;i-=lowbit(i)) tmp2[++pp2]=version[i];
return vv[solve(1,len,1,k)];
}
int main()
{
ios::sync_with_stdio(false);
cin.tie(0); cout.tie(0);
cin>>n>>m;
for(int i=1;i<=n;i++)
{
cin>>a[i];
vv[++len]=a[i];
}
for(int i=1;i<=m;i++)
{
auto& [s,l,r,k]=q[i]; cin>>s;
if(s=="Q") cin>>l>>r>>k;
else
{
cin>>l>>r;
vv[++len]=r;
}
}
sort(vv+1,vv+1+len); len=unique(vv+1,vv+1+len)-vv-1;
for(int i=1;i<=n;i++) a[i]=lower_bound(vv+1,vv+1+len,a[i])-vv;
for(int i=1;i<=m;i++)
if(q[i].s=="C") q[i].r=lower_bound(vv+1,vv+1+len,q[i].r)-vv;
for(int i=1;i<=n;i++) add(i,a[i],1);
for(int i=1;i<=m;i++)
{
auto& [s,l,r,k]=q[i];
if(s=="Q") printf("%d\n",solve(l,r,k));
else
{
add(l,a[l],-1);
add(l,a[l]=r,1);
}
}
return 0;
}
本文介绍了一种结合树状数组与主席树的数据结构优化方案,用于解决动态区间KK小问题。通过对原始数组进行预处理并利用树状数组的特性,实现了高效更新与查询操作。该方法能够将单次查询的时间复杂度降低到O(log²n),整体时间复杂度为O(nlog²n)。
384

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



