主席树详解

本文详细介绍了主席树的概念及其在解决区间第k大问题的应用。通过实例解释了如何建立和查询主席树,并提供了一个C++实现模板。

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

主席树是一种线段树的变形,可以解决区间第k大的问题。下面我谈谈自己的理解,尽可能的讲清楚些。
假设现在有序列 a[5] = {1 , 4 , 5 , 3 , 2},有q个询问{l , r , k}问你[l , r]区间第k小的值是多少?

  1. 容易想到对被询问区间排个序,然后输出第k个数。这种方法最直观,但也是最费时的(时间复杂度爆炸),不可取。
  2. 另一种是先建立一个空树,再将[l , r]区间的值插入到树中,然后查找。

空树:

空树
现在将[2 , 4]区间的值(4 , 5 , 3)插入到这颗树里.

插入后:

插入后
可以看到插入以后,[3,3],[4, 4] ,[5 5]的值变成了1代表[2 , 4]区间的数分布情况。有了这个树我们只需要遍历一遍就可以解决该区间第k大的问题了。但是这种方法有两个致命的缺陷!

  1. 每次查询都要重新建树,时间复杂度很高。
  2. 对每一次查询都要新建立一个树,太吃内存了,空间复杂度爆炸。
    到了这里,只要解决了这两个问题,这个算法就是一个高效的算法了!我们注意到在插入的时候,每插入一个数,影响的只是这棵树上的一条路径上的
    log2n
    个点受影响,利用这个性质,建树的时候可以只在前一个线段树的基础上增添
    log2n
    个点就可以了,这样复杂度大大降低了。
    这里写图片描述
    可以看到将3插入树中的时候,只影响了3个点。

我们可以建立n课树,对于树tree[i]插入的是[1,i]。这样在求区间[l,r]时,tree[r] - tree[l-1]就是我们要找的树,遍历这颗树就行了。自此主席树求静态区间第k大讲完,等再有空了补上带修改的区间第k大。

我的模板:

#include <bits/stdc++.h>
using namespace std;
const int maxn = 1e5+10;
struct A
{
    int x , idx;
    bool operator < (const A &rhs) const
    {
        return x < rhs.x;
    }
};
A a[maxn];

struct tree
{
    int L , R , sum;
}T[maxn*40];
int n , m , cnt , ran[maxn] , root[maxn];

void insert(int num , int &x , int l , int r)
{
    T[cnt++] = T[x] , x = cnt - 1;
    T[x].sum += 1;
    if(l == r) return ;
    int mid = (l + r) / 2;
    if(num <= mid)
        insert(num , T[x].L , l , mid);
    else
        insert(num , T[x].R , mid+1 , r);
}

int query(int i , int j , int k , int l , int r)
{
    if(l == r) return l;

    int t = T[T[j].L].sum - T[T[i].L].sum;
    int mid = (l + r) / 2;
    if(t >= k)
        return query(T[i].L , T[j].L , k , l , mid);
    return query(T[i].R , T[j].R , k-t , mid+1 , r);

}
int main()
{
    int t , i , j  , k;
    T[0].L = T[0].R = T[0].sum = 0;
    root[0] = 0;
    scanf("%d", &t);
    while(t--)
    {
        scanf("%d %d", &n , &m);
        for(i=1; i<=n; i++)
        {
            scanf("%d", &a[i].x);
            a[i].idx = i;
        }
        sort(a+1 , a+n+1);
        for(i=1; i<=n; i++)
            ran[a[i].idx] = i; //主席树必须要离散化。
        cnt = 1;
        for(i=1; i<=n; i++)
        {
            root[i] = root[i-1];
            insert(ran[i] , root[i] , 1 , n); //建立n颗线段树
        }
        while(m--)
        {
            scanf("%d %d %d", &i , &j , &k);
            printf("%d\n", a[query(root[i-1] , root[j] , k , 1 , n)].x);
        }

    }

    return 0;
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值