题目描述
给出一个长度为n的序列,有m个操作:询问区间[l,r]中第k小的数,将a[i]改成y
n,m<=1e5,a[i]<=1e9
题解
普通的查询区间第k小就用主席树+值域线段树就好了,但是需要修改的话,考虑主席树就是维护前缀和,修改一个点就需要将之后的都修改,这样就会$n^{2}logn$,T飞。
换一种方法记录前缀和,用树状数组维护,在树状数组的每个节点维护一个值域线段树,每个节点就维护他所管辖区间的值域线段树,每次修改就只修改logn个节点。查询就用O(logn)个值域线段树一起查询,因为用树状数组求前缀和就是log个节点相加得到。
#include<bits/stdc++.h> using namespace std; const int maxn=100005; const int maxm=90000005; const int oo=1000000000; int n,m,a[maxn]; int cnt,root[maxn]; int ls[maxm],rs[maxm],size[maxm]; template<class T>inline void read(T &x){ x=0;char ch=getchar(); while(!isdigit(ch)) ch=getchar(); while(isdigit(ch)) {x=(x<<1)+(x<<3)+(ch^48);ch=getchar();} } void modify(int &rt,int l,int r,int pos,int val){ if(!rt) rt=++cnt; size[rt]+=val; if(l==r) 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); } void get_modify(int x,int pos,int val){for(;x<=n;x+=x&-x) modify(root[x],0,oo,pos,val);} int tp1,tp2,s1[maxn],s2[maxn]; int get_query(int x,int y,int k){ tp1=tp2=0; for(;x;x-=x&-x) s1[++tp1]=root[x]; for(;y;y-=y&-y) s2[++tp2]=root[y]; int l=0,r=oo; while(1){ if(l==r) return l; int sum=0,mid=(l+r)>>1; for(int i=1;i<=tp1;i++) sum-=size[ls[s1[i]]]; for(int i=1;i<=tp2;i++) sum+=size[ls[s2[i]]]; if(sum>=k){ for(int i=1;i<=tp1;i++) s1[i]=ls[s1[i]]; for(int i=1;i<=tp2;i++) s2[i]=ls[s2[i]]; r=mid; } else { for(int i=1;i<=tp1;i++) s1[i]=rs[s1[i]]; for(int i=1;i<=tp2;i++) s2[i]=rs[s2[i]]; l=mid+1;k-=sum; } } } int main(){ scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ read(a[i]); get_modify(i,a[i],1); } for(int i=1;i<=m;i++){ char op[2]; scanf("%s",op); if(op[0]=='Q') { int l,r,k; scanf("%d%d%d",&l,&r,&k); printf("%d ",get_query(l-1,r,k)); } else { int x,y; scanf("%d%d",&x,&y); get_modify(x,a[x],-1); get_modify(x,a[x]=y,1); } } }
这里用的也是动态开点。代码还是比较清晰,也比较好写。
主要理解将在区间查询和修改值域线段树用树状数组维护即可。
空间复杂度$O(nlog^{2}n))$