Time Limit: 20000MS | Memory Limit: 65536K | |
Total Submissions: 56807 | Accepted: 19589 | |
Case Time Limit: 2000MS |
Description
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?"
For example, consider the array a = (1, 5, 2, 6, 3, 7, 4). Let the question be Q(2, 5, 3). The segment a[2...5] is (5, 2, 6, 3). If we sort this segment, we get (2, 3, 5, 6), the third number is 5, and therefore the answer to the question is 5.
Input
The second line contains n different integer numbers not exceeding 109 by their absolute values --- the array for which the answers should be given.
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
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
给出一个含有 n 个元素的数组,和 m 个三元组询问,每次输出 区间 [ l , r ] 中的第 k 小数。
一、归并树
归并树实际上就是线段树,只不过这个线段树的节点不是某个值,而是一个数组,而且是有序数组。归并树也叫区域树,节点除了是数组外还可以是线段树。因为在建树的时候它是从叶子节点开始把一个一个较小的(有序)数组归并到父节点上,是一个标准的归并排序的操作。所以被叫做归并树。
具体思路:通过二分枚举答案,检查答案合理性的时候,需要用归并树求出区间内有多少个小于等于它的数。因为归并树每个节点中的数组都是有序的,因此可以在每个区间内用二分查找小于等于它的数的个数,最后把他们都加起来就可以了。
#include<cstdio>
#include<iostream>
#include<algorithm>
#include<vector>
using namespace std;
int n, m;
const int maxn = 100000 + 100;
int A[maxn]; //原始数组
int B[maxn]; //用于二分的有序数组
vector<int> S[maxn * 4]; //归并树
//构造归并树
void Build(int p, int l, int r)
{
if(l == r)
{
S[p].push_back(A[l]);
return;
}
int mid = (l + r) >> 1;
Build(p<<1, l, mid);
Build(p<<1|1, mid+1, r);
//重置节点大小
S[p].resize(r-l+1);
//STL的归并函数
merge(S[p<<1].begin(), S[p<<1].end(),
S[p<<1|1].begin(), S[p<<1|1].end(),
S[p].begin());
}
//归并树查询
//查找大于等于x的元素的个数
int Query(int p, int l, int r, int a, int b, int x)
{
if(l >= a && r <= b)
return upper_bound(S[p].begin(), S[p].end(), x) - S[p].begin();
int res = 0;
int mid = (l + r) >> 1;
if(a <= mid) res += Query(p<<1, l, mid, a, b, x);
if(b > mid) res += Query(p<<1|1, mid+1, r, a, b, x);
return res;
}
int main()
{
scanf("%d %d", &n, &m);
for(int i= 1; i<= n; i++)
{
scanf("%d", A+i);
B[i] = A[i];
}
sort(B+1, B+1+n);
Build(1, 1, n);
while(m--)
{
int l, r, k;
scanf("%d %d %d", &l, &r, &k);
int lb = 1, ub = n;
while(lb <= ub)
{
int mid = (lb + ub) >> 1;
int cnt = Query(1, 1, n, l, r, B[mid]);
if(cnt >= k) ub = mid - 1;
else lb = mid + 1;
}
printf("%d\n", B[ub+1]);
}
return 0;
}
二、平方分割的分桶法
这个做法是把 n 个元素装入 根号n 个桶里,每个桶内部进行排序。
具体做法还是二分枚举答案,然后对于区间内包含的完整的桶,直接二分找出小于等于它的数。对于不完整的桶,则需要一个一个比较是否小于等于它。
需要注意的一点是,如果数组下标从 1 开始, 则 r % b == 0 的时候 r 这个点并没有被放在前一个桶内,所以对于右边零散节点计数是,要用 (r + 1) % b == 0 ,从而把 r % b == 0 的这个 r 归为零散的点。
实际上这个做法效率比较挫,下面的代码 TLE 了。。。只是作为一种思路整理下来。
#include<cstdio>
#include<iostream>
#include<vector>
#include<algorithm>
using namespace std;
int n, m;
const int maxn = 100000 + 100;
const int b = 1000; //桶的大小
int A[maxn]; //原始数组
int B[maxn]; //二分用的有序数组
vector<int> Bucket[maxn/b]; //桶
bool Judge(int l, int r, int x, int k)
{
int cnt = 0;
//单独计算左边零散的节点
while(l <= r && l % b)
{
if(A[l] <= x) cnt ++;
l ++;
}
//单独计算右边零散的节点
//注意是 (r+1)%b
while(l <= r && (r + 1) % b)
{
if(A[r] <= x) cnt ++;
r --;
}
//计算桶内节点
while(l <= r)
{
cnt += upper_bound(Bucket[l/b].begin(), Bucket[l/b].end(), x) - Bucket[l/b].begin();
l += b;
}
return cnt >= k;
}
int main()
{
scanf("%d %d", &n, &m);
for(int i= 1; i<= n; i++)
{
scanf("%d", A+i);
B[i] = A[i];
Bucket[i/b].push_back(A[i]);
}
sort(B+1, B+1+n);
//每个桶内进行排序
for(int i= 0; i< n/b; i++)
sort(Bucket[i].begin(), Bucket[i].end());
for(int i= 1; i<= m; i++)
{
int l, r, k;
scanf("%d %d %d", &l, &r, &k);
int lb = 1, ub = n;
while(lb <= ub)
{
int md = (lb + ub) >> 1;
if(Judge(l, r, B[md], k)) ub = md - 1;
else lb = md + 1;
}
printf("%d\n", B[ub+1]);
}
return 0;
}