题目大意:给一个大小为n(1e6)的序列,求区间[L,R]上的排行第k的数(5000次)。
AC通道:http://poj.org/problem?id=2104
方法一:按数值大小直接排序,记下排行第i的原来的位置是id[i];
排好序之后每次询问从左往右数,如果这个落在[L,R]则k--,k=0时输出...
但这莫不是玄学算法...因为最差复杂度明显是过不了的。
于是这就需要将前缀和思想和线段树处理结合的思想了:
传统题目:给一个序列,询问全局排行第k的元素怎么求?
1.nlogn快排预处理,每次O(1)询问
2.二叉排序树,递归下去如果左边子树个数小于等于k往左边走,否则减去之后往右边走[平衡树中的find_kth()]
硬是要你线段树做怎么办?
3.和平衡树类似的思想,将线段树当做一个桶状的结构,记录这个区间内的元素个数,如果左区间的个数小于等于k往左走,否则往右走。
现在我们询问的是区间[L,R]第k大,再看看上面的线段树求全局的过程,如果我们照样是将线段树当桶用,现在我们希望能随时知道在某个数值区间内[L,R]内的数有多少怎么办?
前缀和!
将统计至第L-1个元素和第R个元素的两棵线段树调出来,设为x,y,那么当前数值区间内处于[L,R]内的元素个数为s[y].sz-s[x].sz,然后步骤和上面也就一样了。
所以我们理想的就是对每前i个建立一颗桶状的线段树,查询的时候就方便了。
问题又来了:怎么建n棵线段树而不会MLE呢?
注意到每次都只是在上一次的基础上往桶中放一个元素,所以有很多节点都不会变,只有从根到最后放下的位置的这一条logn的链上会有变化,所以我们可以充分利用之前的信息。
先完全复制原来的点,加入这点之后更改当前点信息,然后若是要往左边走,再递归下去修改左边节点的信息就好。
百度文库里有个ppt有图有真相,可以看一看:
最后附上代码:
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=100010; struct Node{ int l,r,sz; }s[maxn*18]; int n,m,cnt,key; int a[maxn]; int rk[maxn],id[maxn]; int rt[maxn]; bool cmp(const int &x,const int &y){ return a[x]<a[y]; } void build(int l,int r,int &pos/*这个别名使用比较巧妙,细细体会*/){ s[++cnt]=s[pos],pos=cnt,s[cnt].sz++;//++cnt表示这是新建的一个节点,但是传下来的时候,pos保存的是上一棵树的编号,s[pos]是上一棵树在这个位置的节点信息,cnt将在自己的树中将原来的节点取代。 if(l==r) return ; int mid=(l+r)>>1; if(key<=mid) build(l,mid,s[cnt].l);//如果是分到左子树,那么右子树可以直接利用上棵树的信息,不用修改了 else build(mid+1,r,s[cnt].r); } int query(int l,int r,int x,int y,int k){ if(l==r) return l; int mid=(l+r)>>1,sz=s[s[y].l].sz-s[s[x].l].sz;//sz表示数值在[l,r]内,位置在x,y子树之间的元素个数 if(k<=sz) return query(l,mid,s[x].l,s[y].l,k); else return query(mid+1,r,s[x].r,s[y].r,k-sz); } int main(){ #ifndef ONLINE_JUDGE freopen("2104.in","r",stdin); freopen("2104.out","w",stdout); #endif int k,l,r; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++) scanf("%d",&a[i]),id[i]=i; sort(id+1,id+n+1,cmp); for(int i=1;i<=n;i++) rk[id[i]]=i;//离散化的操作,每个人的rank排名就是它们新的值 for(int i=1;i<=n;i++){ rt[i]=rt[i-1];//每次要利用上次的树,先将根节点设成上次的根节点 key=rk[i],build(1,n,rt[i]); } while(m--){ scanf("%d%d%d",&l,&r,&k); printf("%d ",a[id[query(1,n,rt[l-1],rt[r],k)]]); } return 0; }
当然这题也能用整体二分做,当然因为数字可能超过1e9,所以还需要一下离散化...
然后离散化完了,就很简单了...
和这题做法一样
http://www.cnblogs.com/Robert-Yuan/p/5103863.html
#include<cstdio> #include<cstring> #include<algorithm> using namespace std; const int maxn=100010; const int INF=2*1e9; struct Query{ int x,y,k,id,cur; bool tp; }q[maxn<<1],q1[maxn<<1],q2[maxn<<1]; int n,m,tot; int Hash[maxn],sz,que[maxn]; int ans[maxn],a[maxn],b[maxn],tmp[maxn<<1]; int Find_h(int x){ int l=0,r=sz+1,mid; while(l<r){ mid=(l+r)>>1; if(Hash[mid]>x) r=mid; else if(Hash[mid]<x) l=mid; else return mid; } return l; } void add(int x,int d){ for(;x<=sz;x+=x&-x) b[x]+=d; } int ask(int x){ int sum=0; for(int i=x;i;i-=i&-i) sum+=b[i]; return sum; } void div(int H,int T,int l,int r){ if(H>T) return ; if(l==r){ for(int i=H;i<=T;i++) if(!q[i].tp) ans[q[i].id]=l; return ; } int mid=(l+r)>>1; for(int i=H;i<=T;i++){ if(q[i].tp && q[i].y<=mid) add(q[i].x,1); else if(!q[i].tp) tmp[i]=ask(q[i].y)-ask(q[i].x-1); } for(int i=H;i<=T;i++) if(q[i].tp && q[i].y<=mid) add(q[i].x,-1); int l1=0,l2=0; for(int i=H;i<=T;i++){ if(q[i].tp){ if(q[i].y<=mid) q1[++l1]=q[i]; else q2[++l2]=q[i]; } else{ if(q[i].cur+tmp[i]>=q[i].k) q1[++l1]=q[i]; else q[i].cur+=tmp[i],q2[++l2]=q[i]; } } for(int i=1;i<=l1;i++) q[H+i-1]=q1[i]; for(int i=1;i<=l2;i++) q[H+l1+i-1]=q2[i]; div(H,H+l1-1,l,mid); div(H+l1,T,mid+1,r); } int main(){ #ifndef ONLINE_JUDGE freopen("2104.in","r",stdin); freopen("2104.out","w",stdout); #endif int l,r,k; scanf("%d%d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d",&a[i]);que[i]=a[i]; q[++tot].tp=1;q[tot].x=i; } sort(que+1,que+n+1); Hash[++sz]=que[1]; for(int i=2;i<=n;i++){ while(que[i]==que[i-1]) i++; Hash[++sz]=que[i]; } for(int i=1;i<=n;i++) q[i].y=Find_h(a[i]); for(int i=1;i<=m;i++){ tot++; scanf("%d%d%d",&q[tot].x,&q[tot].y,&q[tot].k); q[tot].id=i; } div(1,tot,1,sz); for(int i=1;i<=m;i++) printf("%d ",Hash[ans[i]]); return 0; }