如果题目给出1e5的数据范围,,以前只会用n*log(n)的方法去想
今天学了一下两三种n*n*log(n)的数据结构
他们就是大名鼎鼎的 归并树 划分树 主席树,,,,
首先来说两个问题,,区间第k大 ,,,,
这个问题的通用算法是 划分树,,
说白一点就是把快速排序的中间结果存起来,
举个栗子
原数列 4 1 8 2 6 9 5 3 7
sorted 1 2 3 4 5 6 7 8 9
。。。。。。。。。。。。。。。。。。。。。。。。。。。
qs[0] 4 1 8 2 6 9 5 3 7
qs[1] 4 1 2 5 3.....8 6 9 7
qs[2] 1 2 3.....4 5......6 7.....8 9
qs[3] 1 2.....3.....4.....5......6......7.....8.....9
qs[4] 1.....2
快速排序的过程如上
我们要求【2,8】区间里第4小的数
我们可以很轻易的知道qs[0]中【2,8】中的数被快排分到左边的个数
记这个个数为 rs rs=4 rs>4 我们可以知道要的答案肯定不可能出现在下一次快排的右边,一定出现在快排的左边,我们只需要在左边查找便是了
在左边如何查找??如果我们的快排是稳定的话(事实上是可以做到稳定的) 下一次查找的开始应该不包括【1,1】中在左边的数,也就是下一层的查询左界应该是( 1+【1,1】中被快排分到左边的个数),有界应该是 (左界+【2,8】被快排分到左边的个数 -1),放在上图就是在qs[1]中查找【2,5】中的第4小,
也就是在qs[1]的 [1,5]中查找【2,5】中的第4小
【2,5】被快排分到左边的个数是 3 3<4也就是说 我们要找的数不可能在左边,应该是在右边
而且应该是在右边找第 4-3 小的数 最小的数 右边查找的左界应该是 (mid+1 +【1,1】中被分到右边的个数),而 查询的右界是 (左界 + 【2,5】中被分到右边的个数-1)
也就是在qs[2] 的 [4,5] 区间内查找【5,5】内的最小数。。这个数 就是 5了吧,。。。
我们可以用一个 sum[d][i] 来存储在快排深度为d时 区间内前i位被快排分到左边的数的个数;可以用o(1)的时间算出下次查找的区间
总的时间复杂都市n*log(n)*log(n)
以下是划分树的代码
1 #include <iostream> 2 #include <string.h> 3 #include <algorithm> 4 #include <cstdio> 5 using namespace std; 6 #define cl(a,b) memset(a,b,sizeof(a)) 7 #define MAXN 100005 8 9 typedef struct mydata 10 { 11 int p,v,h; 12 }D; 13 14 int cmpv(D aa,D bb) 15 { 16 if(aa.v==bb.v)return aa.p<bb.p; 17 return aa.v<bb.v; 18 } 19 20 int cmpp(D aa , D bb) 21 { 22 return aa.p<bb.p; 23 } 24 25 D dat[MAXN]; 26 int da[MAXN]; 27 int sum[20][MAXN]; 28 int qso[20][MAXN],n,m; 29 30 void build(int l,int r,int c) 31 { 32 if(l==r)return ; 33 int mid=(l+r)/2; 34 int i,cl=0,cr=0; 35 for(i=l;i<=r;i++) 36 { 37 if(qso[c][i]<=mid) 38 { 39 sum[c][i]=sum[c][i-1]+1; 40 qso[c+1][l+cl]=qso[c][i]; 41 ++cl; 42 } 43 else 44 { 45 sum[c][i]=sum[c][i-1]; 46 qso[c+1][mid+1+cr]=qso[c][i]; 47 ++cr; 48 } 49 if(i==l)sum[c][i]-=sum[c][i-1]; 50 } 51 build(l,mid,c+1); 52 build(mid+1,r,c+1); 53 } 54 55 int query(int d,int l,int r,int ql,int qr,int k) 56 { 57 if(l==r)return qso[d][l]; 58 int mid=(l+r)/2; 59 int sl=sum[d][ql-1]; 60 if(l==ql)sl=0; 61 int sr=sum[d][qr]; 62 int rs=sr-sl; 63 if(k<=rs)return query(d+1,l,mid,l+sl,l+sr-1,k); 64 return query(d+1,mid+1,r,mid+1+ql-l-sl,mid+1+qr-l-sr,k-rs); 65 } 66 67 int main() 68 { 69 int tt; 70 cin>>tt; 71 while(tt--) 72 { 73 cl(sum,0); 74 cl(qso,0); 75 scanf("%d %d",&n,&m); 76 int i,j,k,l,r,x; 77 for(i=1;i<=n;i++){ 78 scanf("%d",&dat[i].v); 79 dat[i].p=i; 80 } 81 sort(dat+1,dat+n+1,cmpv); 82 for(i=1;i<=n;i++) 83 { 84 dat[i].h=i; 85 da[i]=dat[i].v; 86 } 87 sort(dat+1,dat+1+n,cmpp); 88 for(i=1;i<=n;i++)qso[0][i]=dat[i].h; 89 build(1,n,0); 90 for(i=0;i<m;i++) 91 { 92 scanf("%d %d %d",&l,&r,&k); 93 int pos=query(0,1,n,l,r,k); 94 printf("%d\n",da[pos]); 95 } 96 } 97 return 0; 98 }
这道题除了用划分树来做之外 还可以用可持久化线段树来做 貌似还可以用归并树来做
其实归并树就是划分树倒过来。。。。。。。。。。。。。。。。。
可持久化线段树---------------------“被一小撮不怀好意的人起了一个奇怪的名字”--主席树 哈哈
主席树的原理是 每次更新的时候。不是修改值 而是插入值。。
主席树的其他建模。。。。。我不是很清楚 只是弄懂了此题的建模。。
对于这种玄幻的数据结构,,我感觉我解释不清楚,,我觉得给我比较鲜明的一个提示是。。。。。。。每次更新是新建一条从根节点到目标节点的路劲。。对于没有修改的节点就用原先的,需要修改的就新插入。而需要插入的节点数是log(n)级的
这种数据结构的空间复杂度和时间复杂度都是o(n*log(n)*log(n))的
talk is cheap show you the code
1 #include <iostream> 2 #include <string.h> 3 #include <algorithm> 4 #include <cstdio> 5 using namespace std; 6 #define cl(a,b) memset(a,b,sizeof(a)) 7 #define MAXN 100005 8 9 int sum[MAXN*20]; 10 int ls[MAXN*20]; 11 int rs[MAXN*20]; 12 int num[MAXN],cn; 13 int has[MAXN],m,n,ch; 14 int root[MAXN]; 15 16 int phash(int l,int r,int x) 17 { 18 if(l==r)return l; 19 int mid=(l+r)/2; 20 if(has[mid]==x)return mid; 21 if(has[mid]>x)return phash(l,mid,x); 22 return phash(mid+1,r,x); 23 } 24 25 void build(int l,int r,int &tx) 26 { 27 tx=++cn; 28 sum[tx]=0; 29 if(l==r)return ; 30 int mid=(l+r)/2; 31 build(l,mid,ls[tx]); 32 build(mid+1,r,rs[tx]); 33 } 34 35 void inst(int pos,int l,int r,int last,int &tx) 36 { 37 tx=++cn; 38 sum[tx]=sum[last]+1; 39 ls[tx]=ls[last]; 40 rs[tx]=rs[last]; 41 // cout<<l<<' '<<r<<endl; 42 if(l==r)return ; 43 int mid=(l+r)/2; 44 if(pos<=mid)inst(pos,l,mid,ls[last],ls[tx]); 45 else inst(pos,mid+1,r,rs[last],rs[tx]); 46 } 47 48 int query(int l,int r,int k,int last,int tx) 49 { 50 if(l==r)return l; 51 int mid=(l+r)/2; 52 int rum=sum[ls[tx]]-sum[ls[last]]; 53 if(rum>=k)return query(l,mid,k,ls[last],ls[tx]); 54 else return query(mid+1,r,k-rum,rs[last],rs[tx]); 55 } 56 57 int main() 58 { 59 int tt; 60 cin>>tt; 61 while(tt--) 62 { 63 cl(ls,0); 64 cl(rs,0); 65 cl(sum,0); 66 scanf("%d %d",&n,&m); 67 int i,j,k,l,r,x; 68 for(i=1;i<=n;i++){ 69 scanf("%d",&num[i]); 70 has[i]=num[i]; 71 } 72 sort(has+1,has+1+n); 73 i=1;j=1; 74 while(i<=n&&j<=n) 75 { 76 if(has[i]!=has[j])has[++i]=has[j++]; 77 else j++; 78 } 79 ch=i; 80 cn=0; 81 // cout<<ch<<endl; 82 build(1,ch,root[0]); 83 for(i=1;i<=n;i++) 84 { 85 inst(phash(1,ch,num[i]),1,ch,root[i-1],root[i]); 86 } 87 for(i=0;i<m;i++) 88 { 89 scanf("%d %d %d",&l,&r,&x); 90 printf("%d\n",has[query(1,ch,x,root[l-1],root[r])]); 91 } 92 } 93 return 0; 94 }
再多BB 两句 。。。划分树的可以很方便的求出区间第k大 而归并树可以很方便的求出区间比k大的数个数。。。想到了一道cf 的题,,,
而主席树这种玄幻的东西,,两个都可以做,,
最后引用这种玄幻的东西的作者的一句话
..这个东西是当初我弱不会划分树的时候写出来替代的一个玩意..被一小撮别有用心的人取了很奇怪的名字> < 想法是对原序列的每一个前缀[1..i]建立出一颗线段树维护值域上每个数的出现次数,然后发现这样的树是可以减的,然后就没有然后了