主席树是个啥?浅谈主席树

主席树

~

有问题欢迎在评论区一起讨论(๑•̀ㅂ•́)و✧

问题引入

n个数字,m次查询。
查询方式:l, r, k.
即求区间[l, r]中第k大的数字

使用主席树

为了高效率地解决这个问题,我们需要使用主席树。

那么什么是主席树?
一言以蔽之:带历史版本的线段树。
所谓历史版本,即在插入某个数字之前,这棵线段树的样子。

如果用n (数字的数量) 棵线段树实现这种“历史版本”会炸内存的。
不过在思考的过程中还是很有必要实践一下的。
在稍加模拟之后可以发现相邻的两棵子树中有近乎半数的节点没有发生改变,极大地浪费了空间,所以对于这些节点,我们不再每次都新建,而是直接使用原来的节点。
主席树就是这样了。

前置技能

如上所言,我们需要建立n棵线段树,不过多数不变的节点不再新建。
那么我们需要提前知道数据的范围。如果数据的范围超级大呢?为了避免这种情况下的MLE, 我们需要用到离散化的知识(很简单的,不要打退堂鼓呀!)。
离散化链接:离散化
此外,既然是n棵线段树,那么必然要学过线段树的呀~
线段树链接:线段树

要用的变量

int nodeNum;
// 当前节点的全局编号
int L[N << 5], R[N << 5], sum[N << 5];
// L[i]  : 节点i的左子树的根节点的全局编号
// R[i]  : 节点i的右子树的根节点的全局编号
// sum[i]: 以节点i为根节点的树所储存的数字的数量
int a[N], b[N];
// a: 初始数组
// b: 初始数组的副本,用于离散化
int T[N];
// T[i]: 第i+1棵子树的根节点

建树

// 建树, 返回根节点
int build(int l, int r) {
    // num 为当前树的根节点编号
    int num = ++nodeNum;
    if (l != r) {
        int mid = (l + r) >> 1;
        // 建立左子树
        L[num] = build(l, mid); // 当前num节点的左子树的根节点编号
        // 建立右子树
        R[num] = build(mid + 1, r);
    }
    return num;
}

插入节点

// 插入节点x,返回所生成的线段树的根节点
// pre为上一棵子树的根节点
int udNode(int pre, int l, int r, int x) {
    int num = ++nodeNum;
    // 将上一棵子树的左右子树连接到新树的根节点
    L[num] = L[pre];
    R[num] = R[pre];
    // 新树中插入了一个数字,所以sum[num]比sum[pre]大 1
    sum[num] = sum[pre] + 1;
    if (l != r) {
        int mid = (l + r) >> 1;
        // 确定待插入的节点所在的子树
        // 生成新的左/右子树并更新根节点的对应的子树编号
        if (x <= mid) L[num] = udNode(L[pre], l, mid, x);
        else R[num] = udNode(R[pre], mid + 1, r, x);
    }
    return num;
}

区间查询

// 查询[l,r]中第k大/小的数字并返回该数字
int query(int u, int v, int l, int r, int k) {
    // 递归出口:到达叶子节点
    if (l == r) return b[l];
    int mid = (l + r) >> 1;
    // 设 u: l-1, v: r
    // 则 num: 第r+1棵树的左子树的数字的数量减去第l棵树的左子树的数字的数量所得的数字的数量
    int num = sum[L[v]] - sum[L[u]];
    // 若 k 小于等于 num,说明所求数字在左子树
    // 否则说明所求数字在右子树
    if (num >= k) return query(L[u], L[v], l, mid, k);
    else return query(R[u], R[v], mid + 1, r, k - num); 
    // 在右子树时,需要减去左子树的数字的数量。因为全局的第k大/小的数字,在右子树中为第k-num大/小的数字
}

模板题的代码(注释很有用哦!)

题目链接:LG模板题

#include<iostream>
#include<cstdio>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<cstdlib>
#include<string>
#include<queue>
#include<map>
#include<stack>
#include<list>
#include<set>
#include<deque>
#include<vector>
#include<ctime>

using namespace std;
//#pragma GCC optimize(2)
#define IO ios::sync_with_stdio(false);cin.tie(0);cout.tie(0)
#define ull unsigned long long
#define ll long long
#define rep(i, x, y) for(int i=x;i<=y;i++)
#define mms(x, n) memset(x, n, sizeof(x))
#define mmc(A, b) memcpy(A, b, sizeof(b))
#define INF (0x3f3f3f3f)
#define mod (ull)(1e9+7)
typedef pair<int, int> P;
const int N = 2e5 + 10;
int nodeNum;
// 当前节点的全局编号
int L[N << 5], R[N << 5], sum[N << 5];
// L[i]  : 节点i的左子树的根节点的全局编号
// R[i]  : 节点i的右子树的根节点的全局编号
// sum[i]: 以节点i为根节点的树所储存的数字的数量
int a[N], b[N];
// a: 初始数组
// b: 初始数组的副本,用于离散化
int T[N];
// T[i]: 第i+1棵子树的根节点

// 建树, 返回根节点
int build(int l, int r) {
    // num 为当前树的根节点编号
    int num = ++nodeNum;
    if (l != r) {
        int mid = (l + r) >> 1;
        // 建立左子树
        L[num] = build(l, mid); // 当前num节点的左子树的根节点编号
        // 建立右子树
        R[num] = build(mid + 1, r);
    }
    return num;
}

// 插入节点x,返回所生成的线段树的根节点
// pre为上一棵子树的根节点
int udNode(int pre, int l, int r, int x) {
    int num = ++nodeNum;
    // 将上一棵子树的左右子树连接到新树的根节点
    L[num] = L[pre];
    R[num] = R[pre];
    // 新树中插入了一个数字,所以sum[num]比sum[pre]大 1
    sum[num] = sum[pre] + 1;
    if (l != r) {
        int mid = (l + r) >> 1;
        // 确定待插入的节点所在的子树
        // 生成新的左/右子树并更新根节点的对应的子树编号
        if (x <= mid) L[num] = udNode(L[pre], l, mid, x);
        else R[num] = udNode(R[pre], mid + 1, r, x);
    }
    return num;
}

// 查询[l,r]中第k大/小的数字并返回该数字
int query(int u, int v, int l, int r, int k) {
    // 递归出口:到达叶子节点
    if (l == r) return b[l];
    int mid = (l + r) >> 1;
    // 设 u: l-1, v: r
    // 则 num: 第r+1棵树的左子树的数字的数量减去第l棵树的左子树的数字的数量所得的数字的数量
    int num = sum[L[v]] - sum[L[u]];
    // 若 k 小于等于 num,说明所求数字在左子树
    // 否则说明所求数字在右子树
    if (num >= k) return query(L[u], L[v], l, mid, k);
    else return query(R[u], R[v], mid + 1, r, k - num); 
    // 在右子树时,需要减去左子树的数字的数量。因为全局的第k大/小的数字,在右子树中为第k-num大/小的数字
}

int main() {
//    freopen("input.txt", "r", stdin);
    int n, m;
    scanf("%d%d", &n, &m);

    // 离散化
    rep(i, 1, n) {
        scanf("%d", &a[i]);
        b[i] = a[i]; // b 数组为 a 数组的副本
    }
    // 排序后去重
    sort(b + 1, b + 1 + n);
    // 获取去重后的数组大小
    int size = unique(b + 1, b + 1 + n) - b - 1;
    // T[0] 为第一棵线段树(空树)的根节点
    T[0] = build(1, size);
    for (int i = 1; i <= n; i++) {
        // 获取映射值
        int x = lower_bound(b + 1, b + 1 + size, a[i]) - b;
        // T[i] 为第i+1棵线段树的根节点
        T[i] = udNode(T[i - 1], 1, size, x);
    }

    // 查询
    while (m--) {
        int l, r, k;
        // [l, r]区间中第k大/小的数字
        scanf("%d%d%d", &l, &r, &k);
        printf("%d\n", query(T[l - 1], T[r], 1, size, k));
    }
    return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_45934120/article/details/108028914
今日推荐