【模板】可持久化线段树 2

发布时间 2023-11-23 20:11:01作者: onlyblues

【模板】可持久化线段树 2

题目背景

这是个非常经典的可持久化权值线段树入门题——静态区间第 $k$ 小。

数据已经过加强,请使用可持久化权值线段树。同时请注意常数优化

题目描述

如题,给定 $n$ 个整数构成的序列 $a$,将对于指定的闭区间 $[l, r]$ 查询其区间内的第 $k$ 小值。

输入格式

第一行包含两个整数,分别表示序列的长度 $n$ 和查询的个数 $m$。  
第二行包含 $n$ 个整数,第 $i$ 个整数表示序列的第 $i$ 个元素 $a_i$。   
接下来 $m$ 行每行包含三个整数 $ l, r, k$ , 表示查询区间 $[l, r]$ 内的第 $k$ 小值。

输出格式

对于每次询问,输出一行一个整数表示答案。

样例 #1

样例输入 #1

5 5
25957 6405 15770 26287 26465 
2 2 1
3 4 1
4 5 1
1 2 2
4 4 1

样例输出 #1

6405
15770
26287
25957
26287

提示

样例 1 解释

$n=5$,数列长度为 $5$,数列从第一项开始依次为$\{25957, 6405, 15770, 26287, 26465\}$。

  • 第一次查询为 $[2, 2]$ 区间内的第一小值,即为 $6405$。
  • 第二次查询为 $[3, 4]$ 区间内的第一小值,即为 $15770$。
  • 第三次查询为 $[4, 5]$ 区间内的第一小值,即为 $26287$。
  • 第四次查询为 $[1, 2]$ 区间内的第二小值,即为 $25957$。
  • 第五次查询为 $[4, 4]$ 区间内的第一小值,即为 $26287$。


数据规模与约定

  • 对于 $20\%$ 的数据,满足 $1 \leq n,m \leq 10$。
  • 对于 $50\%$ 的数据,满足 $1 \leq n,m \leq 10^3$。
  • 对于 $80\%$ 的数据,满足 $1 \leq n,m \leq 10^5$。
  • 对于 $100\%$ 的数据,满足 $1 \leq n,m \leq 2\times 10^5$,$|a_i| \leq 10^9$,$1 \leq l \leq r \leq n$,$1 \leq k \leq r - l + 1$。

 

解题思路

  最近学了主席树,大概记录一下思想和代码。

  主席树是由线段树发展来的,用来实现可持久化。我们知道每次执行修改操作后线段树维护的信息就会发生变化,而所谓的可持久化实就是保存每次修改操作后的线段树的版本,那么当我们需要知道某次修改操作后的线段树的状态时,就可以通过主席树来得到其历史版本。为了实现可持久化,最暴力的思路是对每个修改操作都新开一棵线段树,把上一次修改操作后的版本拷贝过来,并在此基础上进行本次的修改操作。我们知道一棵线段树的空间复杂度为 $O(n)$,假设有 $m$ 次修改操作,那么我们就要用 $O(m \cdot n)$ 的空间复杂度来存储所有历史版本的线段树,显然会爆空间。

  我们知道单点修改操作最多会使 $O(\log{n})$ 个线段树的节点发生更新,显然如果直接把上个版本的线段树拷贝过来那么会有很多冗余的信息,浪费空间。所以主席树的思想就是在每次修改操作中,只对被修改的节点创建副本(与上一个版本的完全一样),如果不是叶子节点则修改该副本的左儿子或右儿子的指针。

  1. 如果要对左子树更新,则副本的右儿子不变(指向上一个版本的右子树),递归左子树,左儿子指向回传的当前版本的左子树。
  2. 反之要对右子树更新,则副本的左儿子不变(指向上一个版本的左子树),递归右子树,右儿子指向回传的当前版本的右子树。

  例如下图,线段树维护区间 $[1,6]$(信息只保留维护的区间),对位置 $5$ 进行修改,红色的点即为更新的节点:

  再对位置 $3$ 进行修改,蓝色的点即为更新的节点:

  主席树的思想大概如上,空间复杂度优化到 $O(m \log{n})$。另外主席树一般用来处理单点修改的问题,对于区间修改的操作需要用到“标记永久化”,比较麻烦这里就不介绍了(主要是懒得学。同时主席树要动态开点来实现,每个节点中的 $l$ 和 $r$ 其实是左右儿子的节点编号。

  下面将结合主席树来解决区间第 $k$ 小值问题。

  需要用权值线段树,每个节点除了存储左右儿子的节点编号,还记录该节点维护的值域区间内有多少个数出现,记作 $s$。

  考虑询问的区间为 $[1,r]$,那么可以通过权值线段树维护值 $a_1 \sim a_i$ 的出现次数,然后对线段树进行二分找到第 $k$ 小的值。在考虑一般的区间 $[l,r]$ 前,先维护出 $n$ 个版本的线段树,即从 $1 \sim n$ 依次遍历 $a_i$,对线段树进行单点修改($a_i$ 的出现次数加 $1$),用 $\text{root}[i]$ 来表示第 $i$ 个版本(即维护前 $i$ 个元素)的线段树的根节点。

  由于每个版本的线段树的结构都一样,意味着每个节点维护的值域区间完全对应,只有 $s$ 不同。所以 $\text{root}[r]$ 版本中值域区间 $[L,R]$ 对应的 $s$ 减去 $\text{root}[l-1]$ 版本中值域区间 $[L,R]$ 对应的 $s$ 就等于 $a_l \sim a_r$ 在落在 $[L,R]$ 中的次数,也就是主席树中两个代表相同值域区间的节点具有“可减性”。所以只需对“$\text{root}[r]$ 的信息减去 $\text{root}[l-1]$ 的信息”进行二分线段树就可以求出区间 $[l,r]$ 的第 $k$ 小值。

  AC 代码如下,时间复杂度为 $O((n+m) \log{n})$,空间复杂度为 $O(m \log{n})$:

#include <bits/stdc++.h>
using namespace std;

const int N = 2e5 + 10;

int a[N];
int xs[N], sz;
struct Node {
    int l, r, s;
}tr[N * 20];
int root[N], idx;

int build(int l, int r) {
    int u = ++idx;
    if (l == r) return u;
    int mid = l + r >> 1;
    tr[u].l = build(l, mid);
    tr[u].r = build(mid + 1, r);
    return u;
}

int insert(int u, int l, int r, int x) {
    int v = ++idx;
    tr[v] = tr[u];
    if (l == r) {
        tr[v].s++;
        return v;
    }
    int mid = l + r >> 1;
    if (x <= mid) tr[v].l = insert(tr[u].l, l, mid, x);
    else tr[v].r = insert(tr[u].r, mid + 1, r, x);
    tr[v].s = tr[tr[v].l].s + tr[tr[v].r].s;
    return v;
}

int query(int u, int v, int l, int r, int k) {
    if (l == r) return l;
    int s = tr[tr[v].l].s - tr[tr[u].l].s, mid = l + r >> 1;
    if (k <= s) return query(tr[u].l, tr[v].l, l, mid, k);
    return query(tr[u].r, tr[v].r, mid + 1, r, k - s);
}

int find(int x) {
    int l = 1, r = sz;
    while (l < r) {
        int mid = l + r >> 1;
        if (xs[mid] >= x) r = mid;
        else l = mid + 1;
    }
    return l;
}

int main() {
    int n, m;
    scanf("%d %d", &n, &m);
    for (int i = 1; i <= n; i++) {
        scanf("%d", a + i);
        xs[++sz] = a[i];
    }
    sort(xs + 1, xs + sz + 1);
    sz = unique(xs + 1, xs + sz + 1) - xs - 1;
    root[0] = build(1, sz);
    for (int i = 1; i <= n; i++) {
        root[i] = insert(root[i - 1], 1, sz, find(a[i]));
    }
    while (m--) {
        int l, r, k;
        scanf("%d %d %d", &l, &r, &k);
        printf("%d\n", xs[query(root[l - 1], root[r], 1, sz, k)]);
    }
    
    return 0;
}

 

参考资料

  AcWing 255. 第K小数(算法提高课):https://www.acwing.com/video/657/

  可持久化线段树 - OI Wiki:https://oi-wiki.org/ds/persistent-seg/

  主席树:https://www.cnblogs.com/LonecharmRiver/articles/9087536.html