[POJ 2104]K-th Number
Description
You are working for Macrohard company in data structures department. After failing your previous task about key insertion you were asked to write a new data structure that would be able to return quickly k-th order statistics in the array segment.
That is, given an array a[1...n] of different integer numbers, your
program must answer a series of questions Q(i, j, k) in the form: "What
would be the k-th number in a[i...j] segment, if this segment was
sorted?"
Input
The first line of the input file contains n --- the size of the array, and m --- the number of questions to answer (1 <= n <= 100 000, 1 <= m <= 5 000).
The following m lines contain question descriptions, each
description consists of three numbers: i, j, and k (1 <= i <= j
<= n, 1 <= k <= j - i + 1) and represents the question Q(i, j,
k).
Output
For each question output the answer to it --- the k-th number in sorted a[i...j] segment.
Sample Input
7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3
Sample Output
5
6
3
HINT
This problem has huge input,so please use c-style input(scanf,printf),or you may got time limit exceed.
題目大意
求不修改的區間$k$小值。
題解
這道題做法有很多,據說某種奇妙的暴力可以過,還有《挑戰程序設計競賽》上說的平方分割、分桶法等等可以做。這些就先不討論。這裏介紹$5$種常見的做法。
解法一 莫隊(+離散化+樹狀數組+二分)
一看到題,區間求值,就想到萬能的莫隊。
我們將數值哈希一下,將值域範圍縮小。
做莫隊的同時維護一個樹狀數組,記錄出現次數。 詢問的時候可以考慮二分,在樹狀數組中查找第$k$小的數。
1 #include<map> 2 #include<set> 3 #include<ctime> 4 #include<cmath> 5 #include<queue> 6 #include<stack> 7 #include<cstdio> 8 #include<string> 9 #include<vector> 10 #include<cstdlib> 11 #include<cstring> 12 #include<iostream> 13 #include<algorithm> 14 #define LL long long 15 #define RE register 16 #define IL inline 17 #define lowbit(x) (x&-x) 18 using namespace std; 19 const int N=100000; 20 const int M=5000; 21 const int INF=2e9; 22 23 struct tt 24 { 25 int num,pos,id; 26 }a[N+5]; 27 int rem[N+5]; 28 int n,m,lim; 29 IL bool comp(tt a,tt b) {return a.num<b.num;} 30 IL bool accomp(tt a,tt b) {return a.pos<b.pos;} 31 32 struct ss 33 { 34 int l,r,k,id; 35 }q[M+5]; 36 IL bool qcomp(ss a,ss b) {return a.l/lim==b.l/lim ? a.r<b.r : a.l<b.l;} 37 38 int c[N+5]; 39 void Add(int a,int k) {for (;a<=n;a+=lowbit(a)) c[a]+=k;} 40 int Count(int a) 41 { 42 int sum=0; 43 for (;a;a-=lowbit(a)) sum+=c[a]; 44 return sum; 45 } 46 int Dev(int k) 47 { 48 int l=1,r=n,mid,ans; 49 while (l<=r) 50 { 51 mid=(l+r)>>1; 52 if (Count(mid)>=k) r=mid-1,ans=mid; 53 else l=mid+1; 54 } 55 return rem[ans]; 56 } 57 58 int ans[M+5]; 59 60 int main() 61 { 62 scanf("%d%d",&n,&m); 63 lim=sqrt(double(n)); 64 for (RE int i=1;i<=n;i++) scanf("%d",&a[i].num),a[i].pos=i; 65 sort(a+1,a+n+1,comp); 66 a[0].num=-INF; 67 for (RE int i=1;i<=n;i++) a[i].id=a[i-1].id+(a[i].num!=a[i-1].num),rem[a[i].id]=a[i].num; 68 sort(a+1,a+n+1,accomp); 69 for (RE int i=1;i<=m;i++) scanf("%d%d%d",&q[i].l,&q[i].r,&q[i].k),q[i].id=i; 70 sort(q+1,q+m+1,qcomp); 71 int curl=1,curr=0,l,r; 72 for (RE int i=1;i<=m;i++) 73 { 74 l=q[i].l,r=q[i].r; 75 while (curl<l) Add(a[curl++].id,-1); 76 while (curl>l) Add(a[--curl].id,1); 77 while (curr<r) Add(a[++curr].id,1); 78 while (curr>r) Add(a[curr--].id,-1); 79 ans[q[i].id]=Dev(q[i].k); 80 } 81 for (RE int i=1;i<=m;i++) printf("%d\n",ans[i]); 82 return 0; 83 }莫隊
具體復雜度這裏不給出分析,貼張圖:
解法二 歸並樹(+二分)
構造一棵歸並樹。
詢問的時候需要二分答案,再在歸並樹內找比二分出的值小的元素個數。
1 #include<map> 2 #include<set> 3 #include<ctime> 4 #include<cmath> 5 #include<queue> 6 #include<stack> 7 #include<cstdio> 8 #include<string> 9 #include<vector> 10 #include<cstdlib> 11 #include<cstring> 12 #include<iostream> 13 #include<algorithm> 14 #define LL long long 15 #define RE register 16 #define IL inline 17 using namespace std; 18 const int N=100000; 19 const int ST_SIZE=1<<18; 20 21 int n,m; 22 int a[N],c[N]; 23 vector<int>dat[ST_SIZE]; 24 25 void init(int k,int l,int r) 26 { 27 if (r-l==1) dat[k].push_back(a[l]); 28 else 29 { 30 int chl=2*k+1,chr=2*k+2; 31 int m=(l+r)>>1; 32 init(chl,l,m); 33 init(chr,m,r); 34 dat[k].resize(r-l); 35 merge(dat[chl].begin(),dat[chl].end(), 36 dat[chr].begin(),dat[chr].end(),dat[k].begin()); 37 } 38 } 39 40 int cnt(int a,int b,int k,int l,int r,int num) 41 { 42 if (a<=l&&r<=b) 43 return lower_bound(dat[k].begin(),dat[k].end(),num)-dat[k].begin(); 44 else if (l>=b||a>=r) return 0; 45 else 46 { 47 int chl=2*k+1,chr=2*k+2; 48 int m=(l+r)>>1; 49 int res=cnt(a,b,chl,l,m,num); 50 res+=cnt(a,b,chr,m,r,num); 51 return res; 52 } 53 } 54 55 int main() 56 { 57 scanf("%d%d", &n,&m); 58 for (RE int i=0;i<n;i++) 59 { 60 scanf("%d",&a[i]); 61 c[i]=a[i]; 62 } 63 sort(c,c+n); 64 init(0,0,n); 65 int x,y,k; 66 for (RE int i=0;i<m;i++) 67 { 68 scanf("%d%d%d",&x,&y,&k); 69 int lb=0,ub=n; 70 while (ub-lb>1) 71 { 72 int mid=(ub+lb)>>1; 73 int num=c[mid]; 74 if (cnt(x-1,y,0,0,n,num)<k) lb=mid; 75 else ub=mid; 76 } 77 printf("%d\n",c[lb]); 78 } 79 return 0; 80 }歸並樹
歸並樹跑得就比較慢了:
解法三 劃分樹
劃分樹是基於快速排序的,首先將原始數組$a[]$進行排序$sorted[]$,然後取中位數$m$,將未排序數組中小於$m$放在$m$左邊,大於$m$的放在$m$右邊,並記下原始數列中每個數左邊有多少數小於$m$,用數組$num_.left[depth][]$表示,這就是建樹過程。
重點在於查詢過程,設$[L,R]$為要查詢的區間,$[l,r]$為當前區間,$s$表示$[L,R]$有多少數放到左子樹,$ss$表示$[l,L-1]$有多少數被放倒左子樹,如果$s$大於等於$K$,也就是說第$K$大的數肯定在左子樹裏,下一步就查詢左子樹,但這之前先要更新$L$,$R$,新的$newl=l+ss$, $newr=newl+s-1$。如果$s$小於$k$,也就是說第$k$大的數在右子樹裏,下一步查詢右子樹,也要先更新$L$,$R$,$dd$表示$[l,L-1]$中有多少數被放到右子樹,$d$表示$[L,R]$有多少數被放到右子樹,那麽$newl = m+1+dd$,$newr=m+d+dd$, 這樣查詢逐漸縮小查詢區間,直到最後$L==R$ 返回最後結果就行。
給出一個大牛的圖片例子,
註意代碼中的一些變量和題解不一致!
1 #include<map> 2 #include<set> 3 #include<ctime> 4 #include<cmath> 5 #include<queue> 6 #include<stack> 7 #include<cstdio> 8 #include<string> 9 #include<vector> 10 #include<cstdlib> 11 #include<cstring> 12 #include<iostream> 13 #include<algorithm> 14 #define LL long long 15 #define RE register 16 #define IL inline 17 using namespace std; 18 const int N=100000; 19 20 int n,m; 21 int sorted[N+5]; 22 int f[N+5][20]; 23 int num_left[N+5][20]; 24 25 void Build(int l,int r,int d) 26 { 27 if (l==r) return; 28 int mid=(l+r)>>1; 29 int lsame=mid-l+1; 30 for (RE int i=l;i<=r;i++) if (f[i][d]<sorted[mid]) lsame--; 31 int lpos=l,rpos=mid+1,resame=0; 32 for (RE int i=l;i<=r;i++) 33 { 34 if (i==l) num_left[i][d]=0; 35 else num_left[i][d]=num_left[i-1][d]; 36 if (f[i][d]<sorted[mid]) f[lpos++][d+1]=f[i][d],num_left[i][d]++; 37 else if (f[i][d]>sorted[mid]) f[rpos++][d+1]=f[i][d]; 38 else 39 { 40 if (resame<lsame) resame++,f[lpos++][d+1]=f[i][d],num_left[i][d]++; 41 else f[rpos++][d+1]=f[i][d]; 42 } 43 } 44 Build(l,mid,d+1); 45 Build(mid+1,r,d+1); 46 } 47 int Query(int L,int R,int l,int r,int d,int k) 48 { 49 if (l==r) return f[l][d]; 50 int s,ss; 51 if (L==l) s=num_left[r][d],ss=0; 52 else s=num_left[r][d]-num_left[l-1][d],ss=num_left[l-1][d]; 53 int mid=(L+R)>>1; 54 if (s>=k) return Query(L,mid,L+ss,L+ss+s-1,d+1,k); 55 else 56 { 57 int bb=(l-1)-L+1-ss; 58 int b=r-l+1-s; 59 return Query(mid+1,R,mid+1+bb,mid+bb+b,d+1,k-s); 60 } 61 } 62 63 int main() 64 { 65 scanf("%d%d",&n,&m); 66 for (RE int i=1;i<=n;i++) scanf("%d",&f[i][0]),sorted[i]=f[i][0]; 67 sort(sorted+1,sorted+1+n); 68 Build(1,n,0); 69 int s,t,k; 70 while (m--) 71 { 72 scanf("%d%d%d",&s,&t,&k); 73 printf("%d\n",Query(1,n,s,t,0,k)); 74 } 75 return 0; 76 }劃分樹
劃分樹的效率就很高了:
解法四 主席樹(+離散化)
考慮一個問題:查詢$[1,n]$中的第$K$小值
我們先對數據進行離散化,然後按值域建立線段樹,線段樹中維護某個值域中的元素個數。
在線段樹的每個結點上用$cnt$記錄這一個值域中的元素個數。
那麽要尋找第$K$小值,從根結點開始處理,若左兒子中表示的元素個數大於等於$K$,那麽我們遞歸的處理左兒子,尋找左兒子中第$K$小的數;
若左兒子中的元素個數小於$K$,那麽第$K$小的數在右兒子中,我們尋找右兒子中第$K$-(左兒子中的元素數)小的數。
那麽我們回到題目:查詢區間$[L,R]$中的第$K$小值
我們按照從$1$到$n$的順序依次將數據插入可持久化的線段樹中,將會得到$n+1$個版本的線段樹(包括初始化的版本),將其編號為$0~n$。
可以發現所有版本的線段樹都擁有相同的結構,它們同一個位置上的結點的含義都相同。
考慮第$i$個版本的線段樹的結點$P$,$P$中儲存的值表示$[1,i]$這個區間中,$P$結點的值域中所含的元素個數;
假設我們知道了$[1,R]$區間中$P$結點的值域中所含的元素個數,也知道$[1,L-1]$區間中P結點的值域中所包含的元素個數,顯然用第一個個數減去第二個個數,就可以得到$[L,R]$區間中的元素個數。
因此我們對於一個查詢$[L,R]$,同步考慮兩個根$root[L-1]$與$root[R]$,用它們同一個位置的結點的差值就表示了區間$[L,R]$中的元素個數,利用這個性質,從兩個根節點,向左右兒子中遞歸的查找第$K$小數即可。
1 #include<map> 2 #include<set> 3 #include<ctime> 4 #include<cmath> 5 #include<queue> 6 #include<stack> 7 #include<cstdio> 8 #include<string> 9 #include<vector> 10 #include<cstdlib> 11 #include<cstring> 12 #include<iostream> 13 #include<algorithm> 14 #define LL long long 15 #define RE register 16 #define IL inline 17 using namespace std; 18 const int N=100000; 19 const int INF=2e9; 20 21 int n,m; 22 struct node 23 { 24 int key; 25 node *child[2]; 26 }smg[N*20],*pos=smg; 27 node *root[N+5]; 28 struct tt 29 { 30 int num,id,pos; 31 }a[N+5]; 32 int rem[N+5]; 33 34 IL bool comp(tt a,tt b) {return a.num<b.num;} 35 IL bool accomp(tt a,tt b) {return a.pos<b.pos;} 36 37 void Insert(node *&rt,int l,int r,int k) 38 { 39 node *x=rt; 40 rt=++pos; 41 rt->child[0]=x->child[0]; 42 rt->child[1]=x->child[1]; 43 rt->key=x->key+1; 44 if (l==r) return; 45 int mid=(l+r)>>1; 46 if (k<=mid) Insert(rt->child[0],l,mid,k); 47 else Insert(rt->child[1],mid+1,r,k); 48 } 49 int Query(node *lr,node *rr,int l,int r,int k) 50 { 51 if (l==r) return l; 52 int t=rr->child[0]->key-lr->child[0]->key; 53 int mid=(l+r)>>1; 54 if (k<=t) return Query(lr->child[0],rr->child[0],l,mid,k); 55 else return Query(lr->child[1],rr->child[1],mid+1,r,k-t); 56 } 57 58 int main() 59 { 60 scanf("%d%d",&n,&m); 61 for (RE int i=1;i<=n;i++) scanf("%d",&a[i].num),a[i].pos=i; 62 sort(a+1,a+n+1,comp); 63 a[0].num=-INF; 64 for (RE int i=1;i<=n;i++) a[i].id=a[i-1].id+(a[i].num!=a[i-1].num),rem[a[i].id]=a[i].num; 65 sort(a+1,a+n+1,accomp); 66 67 root[0]=pos; 68 root[0]->child[0]=root[0]->child[1]=pos; 69 root[0]->key=0; 70 71 for (RE int i=1;i<=n;i++) 72 { 73 root[i]=root[i-1]; 74 Insert(root[i],1,n,a[i].id); 75 } 76 77 int s,t,k; 78 while (m--) 79 { 80 scanf("%d%d%d",&s,&t,&k); 81 int tmp=Query(root[s-1],root[t],1,n,k); 82 printf("%d\n",rem[tmp]); 83 } 84 return 0; 85 }主席樹
效率:
解法五 整體二分
額...這個...還沒寫...到時候補上...
[POJ 2104]K-th Number