POJ2104_K-th Number_归并树|二分 ||平方分割的分桶法|二分

本文介绍了一种高效解决K-thNumber查询问题的方法,包括归并树和平方分割分桶法两种核心实现方案,并提供了详细的代码示例。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

K-th Number
Time Limit: 20000MS Memory Limit: 65536K
Total Submissions: 56807 Accepted: 19589
Case Time Limit: 2000MS

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?" 
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 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 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

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.

给出一个含有 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;
}


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值