主席树

【问题引入】

给定一个长度为n的数列,询问区间第k大的数。

【分析】

在正式介绍主席树之前,先来介绍一下“值域线段树”。

为便于讲解,举个例子,对于下图的数列,我们可以对它构造这样的“值域线段树”:

后面的数字表示数列在[l,r]中有多少个不同的数。

有了这棵树,我们可以很方便地求出第k大的数。

那么,怎么求区间第k大呢?

我们建立n棵“值域线段树”。第i棵线段树表示[1,i]的状态。这样一来,查询区间[l,r]时,只需将r号线段树与l-1号线段树的对应位置相减即可。

然而,如果真的将这么多棵线段树都造出来,毫无疑问会MLE。

我们发现,建树的过程本质上是对上一棵线段树进行单点查询。回想一下单点查询的过程,可以发现:每次只是修改了一条路径。所以,我们可以只为修改了的节点开辟内存,而对于那些没有修改的节点,和前面的线段树分享即可。

分析一下空间复杂度:由于每次只加入一条链,而每条链的长度是O(logn)级别的。所以空间复杂度是O(nlogn)

【代码】

#include<cstdio>
#include<cstring>
#include<algorithm>
#define mid ((l + r) >> 1)
using namespace std;
const int mn = 200005;
struct seg{
    int lch, rch, sum;
}t[mn << 5];
int root[mn], a[mn], b[mn], cnt;
void make_tree(int &rt, int l, int r)
{
    rt = ++cnt;
    if(l == r)
        return;
    make_tree(t[rt].lch, l, mid), make_tree(t[rt].rch, mid + 1, r);
}
void edit_tree(int &r1, int &r2, int l, int r, int val)
{
    r1 = ++cnt, t[r1] = t[r2], t[r1].sum++;//以r2为上一版本新建节点
    if(l == r)
        return;
    if(val <= mid)  //获取儿子节点的编号
        edit_tree(t[r1].lch, t[r2].lch, l, mid, val);
    else
        edit_tree(t[r1].rch, t[r2].rch, mid + 1, r, val);
}
int query(int r1, int r2, int l, int r, int k)
{
    int x = t[t[r2].lch].sum - t[t[r1].lch].sum;
    if(l == r)
        return b[l];
    if(x >= k)
        return query(t[r1].lch, t[r2].lch, l, mid, k);
    else
        return query(t[r1].rch, t[r2].rch, mid + 1, r, k - x);
}
int main()
{
    int n, m, i, l, r, k;
    scanf("%d%d", &n, &m);
    for(i = 1; i <= n; i++)
        scanf("%d", &a[i]), b[i] = a[i];
    sort(b + 1, b + 1 + n);
    int siz = unique(b + 1, b + 1 + n) - b - 1; make_tree(root[0], 1, siz);
    for(i = 1; i <= n; i++)
    {
        int val = lower_bound(b + 1, b + 1 + siz, a[i]) - b;//离散化
        edit_tree(root[i], root[i - 1], 1, siz, val);//root[i]:前i个位置的状态对应的线段树的根节点
    }
    while(m--)
    {
        scanf("%d%d%d", &l, &r, &k);
        printf("%d\n", query(root[l - 1], root[r], 1, siz, k));
    }
}

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值