主席树[可持久化线段树]学习笔记

一句话算法:

主席树,又名可持久化线段树,是一种基于前缀和思想对历史版本进行保存,可支持单点修改,查询某个历史版本下某位置的值的数据结构。

主要思想:

对于我们上面加粗的命题,我们先考虑暴力的做法:

对于每一个历史状态,建立一棵线段树维护当前状态的信息。

然后你会发现,状态转移和空间耗损都非常巨大,(边\(MLE\)\(TLE\)的感觉怎么样)你将会得到一个\(TM(LE)^2\)的结果。

我们再考虑如何加速状态转移和压缩空间。

考虑到每一次单点修改时仅仅改动了一个点,其影响的仅仅是这个点到根的这一条链,而其他状态都是不变的:所以我们没有必要将每个节点都更新一遍,而是将被改动的节点新建,然后接到上一棵树的节点上,这样每一次状态转移被压缩成了\(log(n)\)的复杂度,空间同理。

例题:

感觉对着空气还是不太好讲,所以我们来看一道题:

给定N个整数构成的序列,将对于指定的闭区间查询其区间内的第K小值。

提起主席树,几乎所有人最先想到的还是这样一道题。对于区间第\(k\)大,我们建立权值线段树(即以权值作为下标的线段树,节点权值代表数字出现次数),用上面的方法进行状态转移,第\(i\)棵权值线段树维护区间\([1, i]\)的信息,每次查询时利用前缀和思想作差得到中间状态\([l, r]\),然后在树上进行查找,若左子树中的元素总数\(\geq k\),说明第\(k\)大在左子树里,于是进入左子树递归,否则进入右子树递归,注意进入右子树递归时新的\(k\)要减掉左子树的\(size\)

主席树的空间复杂度是\(nlog(n)\)级别的,一般我们会直接开\(n << 5\)的空间不然会RE到飞起。虽然空间消耗还是很大,但是比起暴力,依然是一种优越的做法。

题外话:

主席树的可持久化思想在其他算法上也得到了相应的应用,如可持久化数组、可持久化Trie等。

所以才有了那么多毒瘤数据结构

代码:

#include<bits/stdc++.h>
#define N (200000 + 5)
using namespace std;
inline int read() {
    int cnt = 0, f = 1; char c = getchar();
    while (!isdigit(c)) {if (c == '-') f = -f; c = getchar();}
    while (isdigit(c)) {cnt = (cnt << 3) + (cnt << 1) + c - '0'; c = getchar();}
    return cnt * f;
}
int a[N], b[N * 2], n, m, q, rt[N], tot;
int x, y, k, res;
void Discretize() {
    sort (b + 1, b + n + 1);
    q = unique(b + 1, b + n + 1) - b - 1;
    for (register int i = 1; i <= n; i++) 
        a[i] = lower_bound(b + 1, b + q + 1, a[i]) - b;
}
struct node{
    int l, r, sum;
    #define l(p) tree[p].l
    #define r(p) tree[p].r
    #define sum(p) tree[p].sum
}tree[N * 18];

void build(int &p, int l, int r) {
    p = ++tot;
    if (l == r) return;
    int mid = (l + r) >> 1;
    build (l(p), l, mid);
    build (r(p), mid + 1, r);
}

void insert(int &p, int l, int r, int last, int pos) {
    p = ++tot;
    l(p) = l(last), r(p) = r(last);
    sum(p) = sum(last) + 1;
    if (l == r) return;
    int mid = (l + r) >> 1;
    if (pos <= mid) insert(l(p), l, mid, l(last), pos);
    else insert(r(p), mid + 1, r, r(last), pos);
}

int query (int L, int R, int l, int r, int k) {
    if (l == r) return l;
    int mid = (l + r) >> 1;
    int cnt = sum(l(R)) - sum(l(L));
    if (k <= cnt) return query(l(L), l(R), l, mid, k);
    else return query(r(L), r(R), mid + 1, r, k - cnt);
}

int main() {
    n = read(); m = read();
    for (register int i = 1; i <= n; i++) a[i] = b[i] = read();
    Discretize();
    build(rt[0], 1, q);
    for (register int i = 1; i <= n; ++i) insert(rt[i], 1, q, rt[i - 1], a[i]);
    while (m--) {
        x = read(), y = read(), k = read();
        res = query(rt[x - 1], rt[y], 1, q, k);
        printf("%d\n", b[res]);
    }
    return 0;
}

猜你喜欢

转载自www.cnblogs.com/kma093/p/11141175.html