区间第k小数问题
静态区间第k小数
算法概述
我们在“值域”上建立线段树。每个节点维护一段值域区间[L,R],并记录序列中数值落在这段值域区间[L,R]内的点有多少个,记为cnt。
先不考虑下标区间[l,r]的限制。对于询问整个序列A1~An中的第k小数,我们执行线段树的查询操作,对于每个线段树上的节点,只需比较k与左儿子(其值域区间为[L,mid])的cnt,若k<=l.cnt,那么就可以递归查询左子树中的第k小数,否则递归查询右子树中的第(k-l.cnt)小数。
当然,鉴于Ai的规模,我们建立要建立权值线段树就需要先离散化。
考虑下标区间[l,r]。
显然,只建立一棵线段树是不能够支持我们的操作的。于是我们便需要建立主席树,考虑将序列A1,A2,A3,……,An依次插入主席树中,那么每一棵主席树的根节点root[i]所对应的其所维护的序列就是A1~Ai,以root[i]为根节点的主席树(值域区间[L,R])就保存了序列A的前i个数中落在[L,R]内的有多少个。
容易发现一条很重要的性质:由于每棵主席树中的所有节点所代表的值域区间都是一一对应的,这就意味着: 我们取两个序列A中的下标l,r。 以root[r]为根的主席树中保存了序列A的前r个数中落在[L,R]内的有多少个, 以root[l]为根的主席树中保存了序列A的前l个数中落在[L,R]内的有多少个。 所以root[r].cnt-root[l].cnt就是序列A下标区间[l+1,r]中落在值域区间[L,R]内的数有多少个。这样我们就把下标区间的问题解决了。
对于每次询问l,r,k。 我们同时遍历以root[r]为根和以root[l-1]为根的两棵主席树,在每一个点上,计算出两者左子树的cnt之差,然后与k作比较,若k较小,则往左递归,若k较大,则往右递归。就是我们刚才讲过的过程了。
于是这个问题就解决了。 当然该问题还有整体二分、归并树、线段树套平衡树等做法,我们不予讨论。、
整个算法时间复杂度O((n+m)logn),空间复杂度O(nlogn)。
参考代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=2e5+10; struct Node{ int l,r; int sum; #define l(p) tree[p].l #define r(p) tree[p].r #define sum(p) tree[p].sum }tree[N*22];int idx; int root[N]; int a[N],b[N]; int n,m,t; int build(int l,int r) { int p=++idx; if(l==r)return p; int mid=l+r>>1; l(p)=build(l,mid); r(p)=build(mid+1,r); return p; } int insert(int now,int l,int r,int x,int val) { int p=++idx; tree[p]=tree[now]; if(l==r){sum(p)+=val;return p;} int mid=l+r>>1; if(x<=mid)l(p)=insert(l(now),l,mid,x,val); else r(p)=insert(r(now),mid+1,r,x,val); sum(p)=sum(l(p))+sum(r(p)); return p; } int query(int u,int v,int l,int r,int k) { if(l==r)return l; int lcnt=sum(l(v))-sum(l(u)); int mid=l+r>>1; if(k<=lcnt)return query(l(u),l(v),l,mid,k); else return query(r(u),r(v),mid+1,r,k-lcnt); } int find(int x) { return lower_bound(b+1,b+t+1,x)-b; } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&a[i]),b[i]=a[i]; sort(b+1,b+n+1); t=unique(b+1,b+n+1)-(b+1); root[0]=build(1,t); for(int i=1;i<=n;i++)root[i]=insert(root[i-1],1,t,find(a[i]),1); while(m--) { int l,r,k; scanf("%d%d%d",&l,&r,&k); int ans=query(root[l-1],root[r],1,t,k); printf("%d\n",b[ans]); } return 0; }
动态区间第k小数
算法概述
基于我们刚才主席树的做法,让以root[i]为根的主席树维护序列A1~Ai的信息,则当我们要将Ax作出修改时,其修改结果会影响到以root[x]、root[x+1]、……、root[n]为根的共n-x+1棵主席树,如果以朴素的做法,每次都把这些主席树全部维护的话,时间复杂度是O(nm)的,显然不行。
但是这也为我们提供了思路。这与前缀和十分相似,当我们修改中间某个点时,自然就会对其后所有前缀和产生影响。而我们的主席树并不善于维护前缀和,所以我们应该交给擅长维护前缀和的树状数组去做。
对于每棵以root[i]为根的子树,我们让其维护序列A[i-lowbit(i)+1~i]的信息。
这样对于每次单点修改,它最多只会影响到logn个root,也就是只会影响logn棵主席树,故我们只需在每次修改时,凭借树状数组的单点修改操作处理出需要维护哪logn棵主席树,然后同时进行维护即可。
对于查询操作也是同样的,我们通过树状数组的区间查询操作处理出需要查询哪logn棵主席树,然后同时遍历。
树状数组的查询操作查询的就是前缀和,对于询问的区间l,r,我们要分别处理两组logn棵主席树,第一组是l-1的前缀和,第二组是r的前缀和。
然后在遍历过程中需要与k进行比较然后判断递归左子树还是右子树的那一步,我们只需将第二组主席树的左儿子的cnt求和,减去第一组主席树的左儿子的cnt之和,再将结果与k进行比较即可。
因为这道题需要离散化,所以我们得离线做。
整个算法时间复杂度O(mlog2n),空间复杂度O(nlog2n)。
参考代码
#include <iostream> #include <cstdio> #include <cstring> #include <algorithm> using namespace std; const int N=1e5+10; struct Ques{ int tp; int l,r,k; int pos,val; }q[N]; struct Node{ int l,r; int cnt; #define l(p) tree[p].l #define r(p) tree[p].r #define cnt(p) tree[p].cnt }tree[N*20];int idx; int root[N]; int a[N],b[N<<1]; int tmp[2][20],cnt[2]; int n,m,t; int lowbit(int x) { return x&-x; } void modify(int &p,int l,int r,int pos,int val) { if(!p)p=++idx; if(l==r){cnt(p)+=val;return;} int mid=l+r>>1; if(pos<=mid)modify(l(p),l,mid,pos,val); else modify(r(p),mid+1,r,pos,val); cnt(p)=cnt(l(p))+cnt(r(p)); } void pre_modify(int x,int val) { int k=lower_bound(b+1,b+t+1,a[x])-b; for(int i=x;i<=n;i+=lowbit(i))modify(root[i],1,t,k,val); } int query(int l,int r,int k) { if(l==r)return l; int sum=0; for(int i=1;i<=cnt[0];i++)sum+=cnt(l(tmp[0][i])); for(int i=1;i<=cnt[1];i++)sum-=cnt(l(tmp[1][i])); int mid=l+r>>1; if(k<=sum) { for(int i=1;i<=cnt[0];i++)tmp[0][i]=l(tmp[0][i]); for(int i=1;i<=cnt[1];i++)tmp[1][i]=l(tmp[1][i]); return query(l,mid,k); } else { for(int i=1;i<=cnt[0];i++)tmp[0][i]=r(tmp[0][i]); for(int i=1;i<=cnt[1];i++)tmp[1][i]=r(tmp[1][i]); return query(mid+1,r,k-sum); } } int pre_query(int l,int r,int k) { memset(tmp,0,sizeof tmp); cnt[0]=cnt[1]=0; for(int i=r;i;i-=lowbit(i))tmp[0][++cnt[0]]=root[i]; for(int i=l-1;i;i-=lowbit(i))tmp[1][++cnt[1]]=root[i]; return query(1,t,k); } int main() { scanf("%d%d",&n,&m); for(int i=1;i<=n;i++)scanf("%d",&a[i]),b[++t]=a[i]; for(int i=1;i<=m;i++) { char tp[2];scanf("%s",tp); if(tp[0]=='Q') { int l,r,k;scanf("%d%d%d",&l,&r,&k); q[i].tp=1,q[i].l=l,q[i].r=r,q[i].k=k; } else { int x,val;scanf("%d%d",&x,&val); q[i].tp=2,q[i].pos=x,q[i].val=val;b[++t]=val; } } sort(b+1,b+t+1); t=unique(b+1,b+t+1)-(b+1); for(int i=1;i<=n;i++)pre_modify(i,1); for(int i=1;i<=m;i++) { if(q[i].tp==1) { int l=q[i].l,r=q[i].r,k=q[i].k; int ans=pre_query(l,r,k); printf("%d\n",b[ans]); } else { int x=q[i].pos,val=q[i].val; pre_modify(x,-1); a[x]=val; pre_modify(x,1); } } return 0; }