POJ 2104 K-th Number(区间第k大数)(平方分割,归并树,划分树)

题目链接:

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;
}

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-12 18:11:15

POJ 2104 K-th Number(区间第k大数)(平方分割,归并树,划分树)的相关文章

POJ 2104 K-th Number(区间第k大数)(平方切割,归并树,划分树)

题目链接: http://poj.org/problem? id=2104 解题思路: 由于查询的个数m非常大.朴素的求法无法在规定时间内求解. 因此应该选用合理的方式维护数据来做到高效地查询. 假设x是第k个数,那么一定有 (1)在区间中不超过x的数不少于k个 (2)在区间中小于x的数有不到k个 因此.假设能够高速求出区间里不超过x的数的个数.就能够通过对x进行二分搜索来求出第k个数是多少. 接下来,我们来看一下怎样计算在某个区间里不超过x个数的个数. 假设不进行预处理,那么就仅仅能遍历一遍全

POJ 2014.K-th Number 区间第k大 (归并树)

K-th Number Time Limit: 20000MS   Memory Limit: 65536K Total Submissions: 57543   Accepted: 19893 Case Time Limit: 2000MS Description You are working for Macrohard company in data structures department. After failing your previous task about key inse

HDU 2665.Kth number 区间第K小

Kth number Time Limit: 15000/5000 MS (Java/Others)    Memory Limit: 32768/32768 K (Java/Others)Total Submission(s): 11394    Accepted Submission(s): 3465 Problem Description Give you a sequence and ask you the kth big number of a inteval. Input The f

POJ 2104:K-th Number(整体二分)

http://poj.org/problem?id=2104 题意:给出n个数和m个询问求区间第K小. 思路:以前用主席树做过,这次学整体二分来做.整体二分在yr大佬的指点下,终于大概懂了点了.对于二分能够解决的询问,如果有多个,那么如果支持离线处理的话,那么就可以使用整体二分了. 在这题二分可行的答案,根据这个答案,把询问操作丢在左右两个队列里面分别递归继续按这样处理.注释里写的很详细. 1 #include <iostream> 2 #include <cstdlib> 3 #

【POJ 2104】K-th Number

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

POJ 2104:K-th Number(主席树静态区间k大)

题目大意:对于一个序列,每次询问区间[l,r]的第k大树. 分析: 主席树模板题 program kthtree; type point=record l,r,s:longint; end; var t:array[0..100000*50]of point; a,b,id,root:array[0..100000]of longint; n,i,m,x,y,k,v,len:longint; procedure qsort(l,h:longint); var i,j,t,m:longint; b

【POJ 2104】 K-th Number 主席树模板题

达神主席树讲解传送门:http://blog.csdn.net/dad3zz/article/details/50638026 2016-02-23:真的是模板题诶,主席树模板水过.今天新校网不好,没有评测,但我立下flag这个代码一定能A.我的同学在自习课上考语文,然而机房党都跑到机房来避难了\(^o^)/~ #include<cstdio> #include<cstring> #include<algorithm> #define for1(i,a,n) for(i

poj 2104主席树求区间第k小

POJ - 2104 题意:求区间第k小 思路:无修改主席树 AC代码: #include "iostream" #include "iomanip" #include "string.h" #include "stack" #include "queue" #include "string" #include "vector" #include "set&

[划分树] POJ 2104 K-th Number

K-th Number Time Limit: 20000MS   Memory Limit: 65536K Total Submissions: 51732   Accepted: 17722 Case Time Limit: 2000MS Description You are working for Macrohard company in data structures department. After failing your previous task about key inse