题目链接:
http://poj.org/problem?id=2104
解题思路:
因为查询的个数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; }
版权声明:本文为博主原创文章,未经博主允许不得转载。