区间第k大值(主席树入门)

本文介绍如何使用主席树这一高效数据结构解决K-thNumber问题,即在数组的指定区间内快速找到第k个有序元素。通过实例解析,详细阐述了主席树的工作原理和实现步骤。

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

K-th Number POJ - 2104

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 10 9 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

不管是求区间第k大还是区间第k小,会求一个就另一个。

通常我们求全数组的第k大会先将其排序,然后第k大就为a[k],但是现在不是全部区间,而是部分区间,要是通过排序再找第k大数的话就显得太暴力了。

所以这时我们需要用到一种新的数据结构——主席树,咳咳,别被这个主席吓着了,其实就是“巧用线段树”而已。

我们要求区间l到r的第k大就是要在这个区间从最大数往最小数数k个!明白了这个,拷贝一份number[],先排序,再去重,然后我们可以以number[]为基底建n个线段树,第i个线段树保存a[]前i个数,这样当我们查询区间l->r的第k大值的时候,只需在第l-1-和第r颗树中从大到小数不同的个数即可,因为第r颗树相比于第l-1颗树多了a[l->r]的数。

这时你可能会有个问题(没有假设你有):一颗线段树就占4n了,n颗不会爆内存?况且线段树拷贝还会爆时间!?

这就是主席树的巧妙之处了——虚节点!注意到,我们每加一个数进来其实相比于前面的一个线段树,这颗线段树只更新了从根节点到一个叶子节点路径上值,所以这颗线段树的其他节点就与上一颗树共用一个就行了,这样每次更新只会多增加树的深度个节点(logn个),避免了内存浪费和拷贝的时间浪费。

下面是本题求第k小的代码,看懂了了的话,试着将它改成求第k大的(只改query()函数,不要变相的用求第k小的去求k大,追求实际理解),还可以试着简化一下代码——将maketree()函数去掉,想想为什么。

Good Luck!

#include<iostream>
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;

struct Node
{
    int l,r,sum;
}tree[3000005];
int a[100005],number[100005],root[100005],node_num;

void maketree(int l,int r,int &node)
{
    node=++node_num;
    if(l==r)return;
    int mid=(l+r)>>1;
    maketree(l,mid,tree[node].l);
    maketree(mid+1,r,tree[node].r);
}

void addtree(int l,int r,int &node,int pre,int pos)
{
    node=++node_num;
    tree[node]=tree[pre];
    tree[node].sum++;
    if(l==r)return;
    int mid=(l+r)>>1;
    if(pos<=mid)addtree(l,mid,tree[node].l,tree[pre].l,pos);
    else addtree(mid+1,r,tree[node].r,tree[pre].r,pos);
}

int query(int l,int r,int node,int pre,int k)
{
    if(l==r)return l;
    int chal=tree[tree[node].l].sum-tree[tree[pre].l].sum;
    int mid=(l+r)>>1;
    if(k<=chal)return query(l,mid,tree[node].l,tree[pre].l,k);
    else return query(mid+1,r,tree[node].r,tree[pre].r,k-chal);
}

int main()
{
    int n,m;
    scanf("%d%d",&n,&m);
    for(int i=1;i<=n;i++){
        scanf("%d",&a[i]);
        number[i]=a[i];
    }
    sort(number+1,number+1+n);
    int num=unique(number+1,number+1+n)-number-1;

    node_num=0;
    maketree(1,num,root[0]);
    for(int i=1;i<=n;i++){
        int pos=lower_bound(number+1,number+1+num,a[i])-number;
        addtree(1,num,root[i],root[i-1],pos);
    }

    int l,r,k;
    while(m--){
        scanf("%d%d%d",&l,&r,&k);
        int pos=query(1,num,root[r],root[l-1],k);
        printf("%d\n",number[pos]);
    }
    return 0;
}


### 求解区间第 k 小问题的方法 #### 方法一:基于快速选择算法的解决方案 快速选择是一种用于查找数组中第 k 或第 k 小元素的选择算法。该算法的时间复杂度平均情况下为 O(n),最坏情况下为 O()[^1]。 ```python def quick_select(nums, left, right, k_smallest): if left == right: return nums[left] pivot_index = partition(nums, left, right) if k_smallest == pivot_index: return nums[k_smallest] elif k_smallest < pivot_index: return quick_select(nums, left, pivot_index - 1, k_smallest) else: return quick_select(nums, pivot_index + 1, right, k_smallest) def partition(nums, left, right): pivot_value = nums[right] store_index = left for i in range(left, right): if nums[i] < pivot_value: nums[store_index], nums[i] = nums[i], nums[store_index] store_index += 1 nums[right], nums[store_index] = nums[store_index], nums[right] return store_index ``` #### 方法二:线段查询 对于静态区间的频繁查询场景,可以预先构建一棵线段来加速查询过程。预处理时间为 O(n log n),每次查询时间复杂度为 O(log n)。 ```cpp struct SegmentTree { int tree[N * 4]; void build(int node, int start, int end) { ... } int query_kth(int node, int start, int end, int l, int r, int k) { ... } }; ``` #### 方法三:平衡二叉搜索(BST) 通过维护一颗支持动态插入删除操作并能高效统计子节点数量的 BST 来解决问题。这种做法适合于需要不断更新序列的情况,在此结构上执行单次插入/删除以及秩查询的操作均为 O(log n)[^1]。 #### 方法四:分块思想 将整个数组分成若干个小块,每一块内部有序排列;当询问某一段区间内的第 K 小值时,则只需考虑跨越边界部分加上两端完整的块即可得到答案。这种方法可以在常数时间内完成初始化工作,并且能够在线性扫描的基础上获得较好的性能表现。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值