POJ-2104 K-th Number(线段树[归并树]-区间第k大)

题目链接

POJ-2104

题目大意

n 个数,Q个询问,每个询问给一个区间 [L,R] k ,求区间[L,R]从小到大排完序之后的第 k 大的数是多大?

数据范围

1n1e51Q5000
1LiRin1kiRiLi+1

解题思路

对于这种经典的问题,解法有很多种,这里介绍的是基于线段树的解法,算个板子吧!
如果有一个排好序的数组,给一个 x ,直接二分一下就知道x是数组的第几大了。基于这种做法,把数列用线段树维护起来。这时线段树的每个节点保存的不再是某个数值,而是对应区间排好序之后的那个数列。
建立线段树的过程和归并排序类似(如果不了解归并排序,可以看看这篇blog:link),一个节点的数列就是其左右儿子的数列归并之后的结果。这个线段树几乎就是归并排序的再现,所以这样的线段树也叫归并树。
还是来个图,实在点。数列 a[]=1,5,6,2,3,7,8,4 ,建出来的树就是这样:
这里写图片描述

接下来就是查询了。
- 如果要查询的区间和当前节点的区间完全没有交集,则返回 0 个。
- 如果要查询的区间完全包含了当前节点的区间,就像上面一样,对当前节点的数列二分一下就好。
- 否则,递归计算左右儿子,求和即可。


建树复杂度 O(nlogn) ,询问里面一个 logn ,访问节点一个 logn ,二分节点数列又一个 logn 。总复杂度 O(nlogn+Qlog3n) 。空间复杂度 O(n) ,有个常数。

代码:

#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <cmath>
#include <algorithm>
#include <queue>
#include <set>
#include <map>
#include <stack>
#include <vector>
using namespace std;
typedef long long LL;
const int inf = 1 << 30;
const LL INF = 1LL << 60;
const int MaxN = 100010;

int n, m;
int a[MaxN + 5];

vector <int> tree[4 * MaxN + 5];

void Build(int rt, int l, int r) {
    if(l == r) {
        tree[rt].push_back(a[l]);
        return;
    }
    int mid = (l + r) >> 1;
    Build(rt << 1, l, mid);
    Build(rt << 1 | 1, mid + 1, r);
    //分配(r - l + 1)个元素所需的空间
    tree[rt].resize(r - l + 1);
    //用STL的merge函数将左右儿子的数列合并
    merge(tree[rt << 1].begin(), tree[rt << 1].end(), tree[rt << 1 | 1].begin(), tree[rt << 1 | 1].end(), tree[rt].begin());
}

//计算区间[L, R]中不超过x的个数
int query(int rt, int l, int r, int L, int R, int x) {
    if(r < L || l > R) return 0;
    else if(L <= l && r <= R) {
        //二分当前节点的数列中有多少个不超过x的个数
        return upper_bound(tree[rt].begin(), tree[rt].end(), x) - tree[rt].begin() ;
    }
    else {
        //递归左右儿子
        int mid = (l + r) >> 1;
        int lc = query(rt << 1, l, mid, L, R, x);
        int rc = query(rt << 1 | 1, mid + 1, r, L, R, x);
        return lc + rc;
    }
}

int main() 
{
    while(scanf("%d %d", &n, &m) != EOF)
    {
        for(int i = 1; i <= n; i++) scanf("%d", &a[i]);
        Build(1, 1, n);
        sort(a + 1, a + n + 1);

        for(int i = 1; i <= m; i++) {
            int L, R, k;
            //求区间[L, R]中的第k个数
            scanf("%d %d %d", &L, &R, &k);
            int l = 1, r = n;
            int mid = 0, res = 0;
            while(l <= r) {
                mid = (l + r) >> 1;
                int ans = query(1, 1, n, L, R, a[mid]);
                if(ans >= k) res = mid, r = mid - 1;
                else l = mid + 1;
            }
            printf("%d\n", a[res]);
        }
    }
    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值