【日志】可持久化线段树2(静态主席树)

可持久化线段树求区间第 k k k小值(静态主席树)

例题:P3834 【模板】可持久化线段树 2

这道题考虑使用静态主席树就好了
考虑一个问题:给定 n n n个数字,之后询问在 [ l , r ] [l,r] [l,r]内的第 k k k小的值是多少。

暴力写法

考虑暴力写法。简单来说,给这几个数字直接sort一下,就求到了。

然后 t t t组询问直接把时间复杂度卡上天

主席树

主席树一种基于线段树的数据结构,全称是可持久化权值线段树(然而思想更重要)。

对于求区间第k小,可以使用主席树(如果想尝试每一次都开一棵线段树也行,MLE

使用主席树求区间第k小问题

先不考虑怎么求区间第k小。先考虑普通线段树如何求得整个序列第k小的数字。

普通线段树求序列第k小

一个很简单的方法。假定这列数字是从 1 1 1 n n n的(就算不是,离散化一下就好了)。如果我们可以按照其大小一个一个插入线段树中,让每个叶节点记录这个数字的个数(具体来讲,就是叶节点管理的那个数字的个数),然后直接按个数找就可以了(其实就是权值线段树,每个叶节点都是个桶)。

普通线段树求区间第k小

那么根据上面的应用,是不是可以在每一次插入一个数的时候,复制出一棵新的树,然后再插进去。根据类似前缀和的思想,找第 r r r棵树和第 l − 1 l-1 l1棵树,前一个减去后一个的信息不就可以找到了嘛。(当然,会MLE,很显然,但是也很暴力)

使用可持久化线段树

如果注意到可持久化是怎么实现可持久化的话,可以发现,对于修改操作,线段树只有一部分结点会受到影响。对于上面(第二个)例子来说,每次插入一个数的时候也只有一个结点会受到影响。也就是说,可以不用每一次造出一棵树,而是可以使用可持久化思想建树,这样就避免了空间的浪费,查询区间第k小只需要找第 r r r个版本和第 l − 1 l-1 l1个版本的根节点即可。

前置知识

  • 线段树
  • 线段树的可持久化版本
  • 动态开点

建立主席树

建立主席树之前,先声明主席树的结点(为了方便,拆开了,没有包装)

声明

下面这个结点声明是基于求区间第k小而声明的(不同题目各有不同,这点非常重要

const int MAXN = 2e5 + 10;
struct Node {
  	int cnt;
    int lc, rc;
}tr[MAXN << 5];
int tot = 0;
int ver[MAXN];

上面的声明有几个要点:

  • cnt是要当桶存数量的
  • MAXN << 5的作用是要开足空间,可持久化线段树需要一定的空间(所以只要没炸就开大点
  • tot是动态开点用的
  • ver,即为version,就是历史版本

现在可以开始建树了。

建树

由于问题是求区间第k小,需要先建立一个空的线段树(也就是啥都没有,前缀和思想经常用到的 l − 1 l-1 l1)。建树过程和可持久化线段树一样。

void build(int l, int r, int now) {
    if (l == r) return;  
    int mid = l + ((r - l) >> 1);
    tr[now].lc = ++ tot;
    build(l, mid, tr[now].lc);
    tr[now].rc = ++ tot;
    build(mid + 1, r, tr[now].rc);
}
插入

对于主席树来说,每一次插入都需要建立一个新的历史版本。

注意一下,这里需要一个更新(pushup啥的)。

inline void update(int now) {
 	tr[now].cnt = tr[tr[now].lc].cnt + tr[tr[now].rc].cnt;   
}
void Insert(int val, int l, int r, int oldv, int newv) {
    if (l == r) {
        tr[newv].cnt = tr[oldv].cnt + 1;  // 从上个版本继承下来
        return;
    }
    int mid = l + ((r - l) >> 1);
    tr[newv].lc = tr[oldv].lc; tr[newv].rc = tr[oldv].rc;
    if (val <= mid) {
        tr[newv].lc = ++ tot;
        Insert(val, l, mid, tr[oldv].lc, tr[newv].lc);
    } else {
        tr[newv].rc = ++ tot;
        Insert(val, mid + 1, r, tr[oldv].rc, tr[newv].rc);
    }
    update(newv);
}
查询

这个就是查询的程序了。

int querykth(int k, int l, int r, int lv, int rv) {
    if (l == r) return l;  // 因为直接开的桶
    int x = tr[tr[rv].lc].cnt - tr[tr[lv].lc].cnt;  // 检查左边有多少个数
    int mid = l + ((r - l) >> 1);
    if (k <= x)
        return querykth(k, l, mid, tr[lv].lc, tr[rv].lc);
    else
        return querykth(k - x, mid + 1, r, tr[lv].rc, tr[rv].rc);  // 去右边查要剪一下
}

这样就可以了。

例题代码

下面就是这道例题的解决代码。

constexpr int MAXN = 2e5 + 10;
struct Node {
    int val, lc, rc;
}tr[MAXN << 5];
int tot = 1;
int a[MAXN], b[MAXN];
int ver[MAXN];

inline void update(int now) {
    tr[now].val = tr[tr[now].lc].val + tr[tr[now].rc].val;
}

void build(int pll, int prr, int now) {
    if (pll == prr) 
        return;
    int mid = pll + ((prr - pll) >> 1);
    tr[now].lc = ++ tot;
    build(pll, mid, tr[now].lc);
    tr[now].rc = ++ tot;
    build(mid + 1, prr, tr[now].rc);
}

void modify(int pos, int pll, int prr, int old, int newv) {
    if (pll == prr) {
        tr[newv].val = tr[old].val + 1;
        return;
    }
    tr[newv].lc = tr[old].lc; tr[newv].rc = tr[old].rc;
    int mid = pll + ((prr - pll) >> 1);
    if (pos <= mid) {
        tr[newv].lc = ++ tot;
        modify(pos, pll, mid, tr[old].lc, tr[newv].lc);
    } else {
        tr[newv].rc = ++ tot;
        modify(pos, mid + 1, prr, tr[old].rc, tr[newv].rc);
    }
    update(newv);
}

int querykth(int k, int pll, int prr, int lefv, int rigv) {
    if (pll == prr)
        return pll;
    int mid = pll + ((prr - pll) >> 1);
    int x = tr[tr[rigv].lc].val - tr[tr[lefv].lc].val;
    if (x >= k)
        return querykth(k, pll, mid, tr[lefv].lc, tr[rigv].lc); 
    return querykth(k - x, mid + 1, prr, tr[lefv].rc, tr[rigv].rc);
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(nullptr);

    int n, t;
    cin >> n >> t;
    for (int i = 1; i <= n; ++ i) {
        cin >> a[i]; b[i] = a[i];
    }
    sort(b + 1, b + 1 + n);
    int len = unique(b + 1, b + 1 + n) - b - 1;  // 注意这题要离散化
    ver[0] = tot;
    build(1, len, ver[0]);
    for (int i = 1; i <= n; ++ i) {
        a[i] = lower_bound(b + 1, b + 1 + len, a[i]) - b;
        ver[i] = ++ tot;  // 新建个树
        modify(a[i], 1, len, ver[i - 1], ver[i]);
    }
    while (t --) {
        int l, r, k;
        cin >> l >> r >> k;
        cout << b[querykth(k, 1, len, ver[l - 1], ver[r])] << '\n';
    }

    return 0;
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值