POJ 2104 K-th Number(区间第k大数)(平方切割,归并树,划分树)
Posted zhchoutai
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了POJ 2104 K-th Number(区间第k大数)(平方切割,归并树,划分树)相关的知识,希望对你有一定的参考价值。
题目链接:
解题思路:
由于查询的个数m非常大。朴素的求法无法在规定时间内求解。
因此应该选用合理的方式维护数据来做到高效地查询。
假设x是第k个数,那么一定有
(1)在区间中不超过x的数不少于k个
(2)在区间中小于x的数有不到k个
因此。假设能够高速求出区间里不超过x的数的个数。就能够通过对x进行二分搜索来求出第k个数是多少。
接下来,我们来看一下怎样计算在某个区间里不超过x个数的个数。
假设不进行预处理,那么就仅仅能遍历一遍全部元素。
还有一方面,假设区间是有序的。那么就能够通过二分搜索法高效地求出不超过x的数的个数了。可是,假设对于每一个查询都分别做一次排序,就全然无法减少复杂度。所以,能够考虑使用平方切割和线段树进行求解。
1.平方切割
首先我们来看看怎样使用平方切割来解决问题。
把数列每b个一组分到各个桶里。每个桶内保存有排序后的数列。这样。假设要求在某个区间中不超过x的数的个数。就能够这样求得。
(1)对于全然包括在区间内的桶。用二分搜索法计算。
(2)对于全部的桶不全然包括在区间内的元素,逐个检查。
假设把b设为sqrt(b),复杂度就变成了
O((n/b)logb + b) = O(sqrt(n)logn)
当中。对每一个元素的处理仅仅要O(1)时间,而对于每一个桶的处理则须要O(logb),所以比起让桶的数量和桶内元素的个数尽可能接近。我们更应该把桶的数量设置成比桶内元素个数略少一些,这样能够使得程序更加高效。假设把b设为sqrt(nlogn)。复杂度就变成
O((n/b)logb + b) = O(sqrt(nlogn))
接下来仅仅须要对x进行二分搜索就能够了。
由于答案一定时数列a里的某个元素,所以二分搜索须要运行O(logn)次。因此。假设b = sqrt(nlogn),包含预处理在内整个算法的复杂度就是O(nlogn + msqrt(n)log1.5次方(n))
AC代码:
#include <iostream> #include <cstdio> #include <vector> #include <algorithm> using namespace std; const int B = 1000;//桶的大小 const int N = 100005; const int M = 5005; //输入 int n,m; int a[N]; int L[M],R[M],K[M]; int nums[N];//对A排序之后的结果 vector<int> bucket[N/B];//每个桶排序之后的结果 void solve(){ for(int i = 0; i < n; i++){ bucket[i/B].push_back(a[i]); nums[i] = a[i]; } sort(nums,nums+n); //尽管每B个一组剩下的部分所在的桶没有排序。可是不会产生问题 for(int i = 0; i < n/B; i++) sort(bucket[i].begin(),bucket[i].end()); for(int i = 0; i < m; i++){ //求[l,r]区间中第k个数 int l = L[i]-1,r = R[i],k = K[i]; int lb = -1,ub = n-1; while(ub-lb > 1){ int mid = (lb+ub)/2; int x = nums[mid]; int tl = l,tr = r,c = 0; //区间两端多出的部分 while(tl < tr && tl % B != 0) if(a[tl++] <= x) c++; while(tl < tr && tr % B != 0) if(a[--tr] <= x) c++; //对每个桶进行计算 while(tl < tr){ int b = tl/B; c += upper_bound(bucket[b].begin(),bucket[b].end(),x)-bucket[b].begin(); tl += B; } if(c >= k) ub = mid; else lb = mid; } printf("%d\n",nums[ub]); } } int main(){ while(~scanf("%d%d",&n,&m)){ for(int i = 0; i < n; i++) scanf("%d",&a[i]); for(int i = 0; i < m; i++) scanf("%d%d%d",&L[i],&R[i],&K[i]); solve(); } return 0; }
2.归并树
以下我们考虑一下怎样使用线段树解决问题。我们把数列用线段树维护起来。
线段树的每一个节点都保存了相应区间排好序后的结果。曾经我们接触过的线段树节点上保存的都是数值,而这次则有所不同。每一个节点保存了一个数列。
建立线段树的过程和归并排序类似,而每一个节点的数列就是其两个儿子节点的数列合并后的结点。建树的复杂度是O(nlogn)。顺带一提,这颗线段树正是归并排序的完总体现。
(归并树)。
要计算在某个区间中不超过x的数的个数,仅仅须要递归地进行例如以下操作就能够了。
(1)假设所给的区间和当前节点的区间全然没有交集。那么返回0个。
(2)假设所给的区间全然包括了当前节点相应的区间。那么使用二分搜索法对该节点上保存的数组进行查找。
(3)否则对两个儿子递归地进行计算之后求和就可以。
因为对于同一深度的节点最多仅仅訪问常数个,因此能够在O(log二次方n)时间里求出不超过x的数的个数。所以整个算法的复杂度是O(nlogn + mlog三次方(n))。
归并树
以1 5 2 6 3 7为例:
把归并排序递归过程记录下来即是一棵归并树:
[1 2 3 5 6 7]
[1 2 5] [3 6 7]
[1 5] [2] [6 3] [7]
[1][5] [6][3]
用相应的下标区间建线段树:(这里下标区间相应的是原数列)
[1 6]
[1 3] [4 6]
[1 2] [3] [4 5][6]
[1][2] [4][5]
每次查找[l r]区间的第k大数时,在[1 2 3 4 5 6 7]这个有序的序列中二分所要找的数x,然后相应到线段树中去找[l r]中比x小的数有几个,即x的rank。由
于线段树中随意区间相应到归并树中是有序的,所以在线段树中的某个区间查找比x小的数的个数也能够用二分在相应的归并树中找。这样一次查询的
时间复杂度是log(n)^2。
要注意的是,多个x有同样的rank时,应该取最大的一个。
AC代码:
#include <iostream> #include <cstdio> using namespace std; const int N = 100005; struct node{ int l,r; }tree[N<<2]; int n,q; int a[N],mer[20][N]; void build(int m,int l,int r,int deep){ tree[m].l = l; tree[m].r = r; if(l == r){ mer[deep][l] = num[l]; return; } int mid = (l+r)>>1; build(m<<1,l,mid,deep+1); build(m<<1|1,mid+1,r,deep+1); //归并排序,在建树的时候保存 int i = l,j = (l+r)/2+1,p = 1; while(i <= (l+r)/2 && j <= r){ if(mer[deep+1][i] > mer[deep+1][j]) mer[deep][p++] = mer[deep+1][j++]; else mer[deep][p++] = mer[deep+1][i++]; } while(i <= (l+r)/2) mer[deep][p++] = mer[deep+1][i++]; while(j <= r) mer[deep][p++] = mer[deep+1][j++]; } int query(int m,int l,int r,int deep,int key){ if(tree[step].r < l || tree[m].l > r) return 0; if(tree[m].l >= l && tree[m].r <= r) //找到key在排序后的数组中的位置 return lower_bound(&mer[deep][tree[m].l],&mer[deep][tree[m].r+1,key) - &mer[deep][tree[m].l]; return query(m<<1,l,r,deep+1,key)+query(m<<1|1,l,r,key); } int solve(int l,int r,int k){ int low = 1,high = n,mid; while(low < high){ mid = (low+high+1)>>1; int cnt = query(1,l,r,1,mer[1][mid]); if(cnt <= k) low = mid; else high = mid-1; } return mer[1][low]; } int main(){ while(~scanf("%d%d",&n,&q)){ for(int i = 1; i <= n; i++) scanf("%d",&a[i]); build(1,1,n,1); while(q--){ int l,r,k; scanf("%d%d%d",&l,&r,&k); printf("%d\n",solve(l,r,k-1)); } } return 0; }
3.划分树
事实上。归并树是在建树的过程中保存归并排序,划分树是在建树的过程中保存高速排序。
划分树
相同以1 5 2 6 3 7为例:
依据中位数mid。将区间划分成左子树中的数小于等于mid。右子树中的数大于等于mid。得到这样一棵划分树:
[1 5 2 6 3 7]
[1 2 3] [5 6 7]
[1 2] [3] [5 6] [7]
[1] [2] [5] [6]
注意要保持下标的先后顺序不变
对每个区间。用sum[i]记录区间的左端点left到i有几个进入了左子树,即有几个数小于等于mid
用相应的下标区间建线段树:(这里下标区间相应的是排序后的数列)
[1 6]
[1 3] [4 6]
[1 2] [3] [4 5][6]
[1][2] [4][5]
每次查找[l r]区间的第k大数时。先查看当前区间[left right]下的sum[r] - sum[l - 1]是否小于等于k,假设是,则递归到左子树,并继续在[left + sum[l - 1],
left + sum[r] - 1]中找第k大数。否则,进入右子树,继续在[mid + l - left + 1 - sum[l - 1], mid + r - left + 1 - sum[r]]找第k - sum[r] + sum[l - 1]大数,这样
一次查询仅仅要logn的复杂度
AC代码:
#include <iostream> #include <cstdio> #include <algorithm> using namespace std; const int N = 100005; struct node{ int l,r,mid; }tree[N<<2]; int sa[N],num[20][N],cnt[20][N];//sa中是排序后的,num记录每一层的排序结果。cnt[deep][i]表示第deep层。前i个数中有多少个进入左子树 int n,q; void debug(int d){ for(int i = 1; i <= n; i++) printf("%d ",num[d][i]); printf("\n"); } void build(int m,int l,int r,int deep){ tree[m].l = l; tree[m].r = r; if(l == r) return ; int mid = (l+r)>>1; int mid_val = sa[mid],lsum = mid-l+1; for(int i = l; i <= r; i++) if(num[deep][i] < mid_val) lsum--;//lsum表示左子树中还须要多少个中值 int L = l,R = mid+1; for(int i = l; i <= r; i++){ if(i == l) cnt[deep][i] = 0; else cnt[deep][i] = cnt[deep][i-1]; if(num[deep][i] < mid_val || (num[deep][i] == mid_val && lsum > 0)){ //左子树 num[deep+1][L++] = num[deep][i]; cnt[deep][i]++; if(num[deep][i] == mid_val) lsum--; } else num[deep+1][R++] = num[deep][i]; } //debug(deep); build(m<<1,l,mid,deep+1); build(m<<1|1,mid+1,r,deep+1); } int query(int m,int l,int r,int deep,int k){ if(l == r) return num[deep][l]; int s1,s2;//s1为[tree[step].left,l-1]中分到左子树的个数 if(tree[m].l == l) s1 = 0; else s1 = cnt[deep][l-1]; s2 = cnt[deep][r]-s1;//s2为[l,r]中分到左子树的个数 if(k <= s2)//左子树的数量大于k,递归左子树 return query(m<<1,tree[m].l+s1,tree[m].l+s1+s2-1,deep+1,k); int b1 = l-1-tree[m].l+1-s1;//b1为[tree[m].l,l-1]中分到右子树的个数 int b2 = r-l+1-s2; //b2为[l,r]中分到右子树的个数 int mid = (tree[m].l+tree[m].r)>>1; return query(m<<1|1,mid+1+b1,mid+1+b1+b2-1,deep+1,k-s2); } int main(){ while(~scanf("%d%d",&n,&q)){ for(int i = 1; i <= n; i++){ scanf("%d",&num[1][i]); sa[i] = num[1][i]; } sort(sa+1,sa+n+1); build(1,1,n,1); while(q--){ int l,r,k; scanf("%d%d%d",&l,&r,&k); printf("%d\n",query(1,l,r,1,k)); } } return 0; }
以上是关于POJ 2104 K-th Number(区间第k大数)(平方切割,归并树,划分树)的主要内容,如果未能解决你的问题,请参考以下文章
POJ 2104 K-th Number(区间第k大数)(平方切割,归并树,划分树)