POJ 2104 K-th Number(快排 or 平方分割 or 归并树—求区间第k大数)

K-th Number
Time Limit: 20000MS   Memory Limit: 65536K
Total Submissions: 45757   Accepted: 15221
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条询问,每条询问包含三个数字(i,j,k),表示输出在第i个数字到第j个数字中第k大的数。

题解:  如果x是第k大的数,那么一定有:
                   1.在区间中有不超过x的数不少于k个。
                   2.在区间中小于x的数有不到k个。
因此我们可以对x进行二分搜索来求出第k个数是多少(快速求出区间中不超过x的数的个数),下面我们介绍三种方法来解决这个问题。

NO.1:  sort排序法:

首先可以确定这一题用单单用sort排序能AC是数据比较弱,偶然。  我们对所有数进行编号,再快排。然后在每一次询问中从最小的数开始遍历,找到第k个出现在区间[i,j]中的数即可。 时间复杂度为O(n*m),仅仅能AC,并非意味着正解。

代码如下:

#include <iostream>
#include <cstdio>
#include <cstring>
#include <algorithm>
using namespace std;
struct node
{
    int val,id;
}a[100010];

int cmp(node x, node y)
{
    return x.val < y.val;
}

int main()
{
    int n,m,i,j,l,r,k;
    while(scanf("%d%d",&n,&m)!=EOF)
    {
        for(i = 0; i < n; ++i)
        {
            scanf("%d",&a[i].val);
            a[i].id = i;
        }
        sort(a, a+n, cmp);
        while(m--)
        {
            scanf("%d%d%d",&l, &r, &k);
            l--; r--;
            for(j=0; j<n; ++j)
            {
                if(a[j].id>=l&&a[j].id<=r)
                    k--;
                if(!k)
                {
                    printf("%d\n",a[j].val);
                    break;
                }

            }
        }
    }
    return 0;
}



NO.2: 平方分割法:

平方分割来源于分桶法。  分桶法是把一排物品(数据)或者平面分成桶,每个桶维护自己内部的信息,以达到高效计算的目的。(将整体元素分割为元素集合)。  平方分割是把一排的n个元素每sqrt(n)个分在一个桶内进行维护的方法统称。 这样的方法可以使对区间的操作的复杂度降至O(sqrt(n))。

把数列每b个一组分到各个桶里,每个桶内保存排序后的数列。这样对于在某个区间中不超过x的数的个数,就可以这样求得。

1.对于完全包含在区间内的桶,用二分搜索计算。
2.对于所在的桶不完全包含在区间内的元素,逐个检查。

可能是我的代码写搓了,悲惨的TLE了,第一次写平方分割,还是记录一下。代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm>
#include<vector>
using namespace std;
#define maxn 100010
#define B 1000//桶的大小
int n,m;
int a[maxn],num[maxn];//num[]表示对a排序之后的结果
vector<int> bucket[maxn/B];

int main()
{
    int i,j,l,r,k;
    while(scanf("%d%d",&n, &m)!=EOF)
    {
        for(i = 0; i < n; ++i)
        {
            scanf("%d",&a[i]);
            bucket[i / B].push_back(a[i]);//分桶
            num[i] = a[i];
        }
        sort(num, num + n);
        for(i = 0; i < n / B; ++i)
            sort(bucket[i].begin(), bucket[i].end() );//对每个桶进行排序
        while(m--)
        {
            scanf("%d%d%d",&l, &r, &k);
            int left = -1, right = n-1, mid;//求[left,right)区间中的第k个书
            while(right - left > 1)
            {
                mid = (left + right) / 2;
                int x = num[mid];
                int tl = l-1, 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)//对每一个桶进行计算
                {
                    i = tl / B;
                    c += upper_bound(bucket[i].begin(), bucket[i].end(), x) - bucket[i].begin();
                    tl += B;
                }
                if(c >= k)
                    right = mid;
                else
                    left = mid;
            }
            printf("%d\n",num[right]);
        }
        return 0;
    }
}


NO.3:归并树:

归并树来源于线段树。在建树的过程中保存归并排序。我们把数列用线段树维护起来。线段树的每个节点那都保存了对应区间排好序的结果。每个节点都保存了一个排好序的队列。建立线段树的过程与归并排的类似。每个节点的数列就是两个儿子节点的数列合并的结果。这种线段树就是归并排序的完整再现。,所以这样的线段树也叫归并树。

要计算某个区间中不超过x的数的个数,只需要递归地进行如下操作就可以了:
1.如果给定区间和当前节点的区间完全没有交集,那么返回0个。 
2.如果所给的区间完全包含了当前节点的对应区间,那么使用二分搜索法对该节点上保存的数组经行查找。
3.否则对两个儿子递归地进行计算求和。

代码如下:

#include<cstdio>
#include<cstring>
#include<algorithm> 
#include<vector>
using namespace std;
#define maxn 100010
#define lson l,mid,i<<1
#define rson mid+1,r,i<<1|1
vector<int>T[maxn<<2];

void build(int l,int r,int i)//建立线段树,i是节点编号,与区间[l,r)相对应 
{
	if(l==r)
	{
		int val;
		scanf("%d",&val);
		T[i].clear();
		T[i].push_back(val);
		return ;
	}
	int mid=(l+r)>>1;
	build(lson);
	build(rson);
	T[i].resize(r-l+1);//调整容器T[i]的大小为 r-l+1 
	merge(T[i<<1].begin(),T[i<<1].end(),T[i<<1|1].begin(),T[i<<1|1].end(),T[i].begin() );
	//将两个儿子的数列合并 
}

//计算区间[ql,qr)中不超过val的个数 
int query(int ql,int qr,int val,int l,int r,int i)
{
	if(ql==l&&qr==r)
		return upper_bound(T[i].begin(),T[i].end(),val)-T[i].begin();
	int mid=(l+r)>>1;
	if(qr<=mid)
		return query(ql,qr,val,lson);
	else if(ql>mid)
		return query(ql,qr,val,rson);
	return query(ql,mid,val,lson)+query(mid+1,qr,val,rson);
}

int main()
{
	int n,m,a,b,k,c,left,right,mid;
	while(scanf("%d%d",&n,&m)!=EOF)
	{
		build(1,n,1);
		while(m--)
		{
			scanf("%d%d%d",&a,&b,&k);
			left=-1; right=n-1;
			while(right-left>1)//二分查找 
			{
				mid=(left+right)>>1;
				c=query(a,b,T[1][mid],1,n,1);
				if(c>=k)
					right=mid;
				else
					left=mid;
			}
			printf("%d\n",T[1][right]);
		}
	}
	return 0;
}


上述的归并树法也是仅仅卡着时限AC的,看了很多人写的是划分树(在建树过程中保存快速排序)应该更快。等学习后再来填坑。



你可能感兴趣的:(POJ 2104 K-th Number(快排 or 平方分割 or 归并树—求区间第k大数))