可持久化数据结构是保存数据结构修改的每一个历史版本,新版本与旧版本相比,修改了某个区域,但是大多数的区域是没有改变的,
所以可以将新版本相对于旧版本未修改的区域指向旧版本的该区域,这样就节省了大量的空间,使得可持久化数据结构的实现成为了可能。
如下图,就是可持久化链表
插入前
插入后
尽可能利用历史版本和当前版本的相同区域来减少空间的开销。
而主席树(可持久化线段树)的原理同样是这样。
有n个数字, 我们将其离散化,那么总有[1,n]个值,如果建一棵线段树,每个结点维护子树中插入的值的个数。 求总区间第k大
那么如果k>=左子树中值的个数,那么就去左子树中找, 否则就去右子树中找第(k-左子树值的个数)大值。
如果要求区间[l,r]第k大,那么就要维护n棵线段树
每棵线段树维护的都是[1,n]值出现的个数, 所以每棵线段树的形态结构是相同的
第i棵线段树的根为T[i] , 维护的是前i个数字离散化后出现的次数。
那么主席树有两个性质
线段树的每个结点,保存的都是这个区间含有的数字的个数
主席树的每个结点,也就是每棵线段树的大小和形态是一样的,也就是主席树的每个结点(线段树与线段树之间)是可以相互进行加减运算的
假设要求区间[l,r]的第k大, T[r]左子树值的个数 - T[l-1]左子树值的个数大于>=k , 那么就说明 区间[l,r]的数离散化后有大于n个数插入了左子树,
所以应该去左子树去找第k个大, 反之,去右子树找 第(k- (T[r]左子树值的个数 - T[l-1]左子树值的个数) )大。
至于主席树的构建, 第T[i+1]棵树相比第T[i]棵树,多插入了一个数字, 也就相当于修改了一条链。
如果第a[i+1]个数插入了T[i+1]的左子树, 那么T[i+1]的右子树和T[i]的右子树是一样的, 所以可以直接指向T[i]的右子树, 然后子树的构造也是这个道理。
#include <stdio.h> #include <string.h> #include <iostream> #include <algorithm> #include <vector> #include <queue> #include <set> #include <map> #include <string> #include <math.h> #include <stdlib.h> #include <time.h> using namespace std; /* 可持久化线段树,函数式线段树,主席树 动态第k大 线段树维护的是值出现的次数 */ const int N = 100010; int a[N], t[N]; int T[N * 30], lson[N * 30], rson[N * 30], c[N * 30]; int total, n, q, m; void initHash() { for (int i = 1; i <= n; ++i) t[i] = a[i]; sort(t + 1, t + n + 1); m = unique(t + 1, t + n + 1) - t - 1; } int Hash(int x) { return lower_bound(t + 1, t + m + 1, x) - t; } int build(int l, int r)//建一棵区间长度为m的线段树, 每个节点的值都是0 { int root = total++; c[root] = 0; if (l != r) { int mid = (l + r) >> 1; lson[root] = build(l, mid); rson[root] = build(mid + 1, r); } return root; } // T[i] 和T[i+1]相比, 只更新了一条链, 所以可以借用T[i+1]的一些子树来节省空间 // 按下标建树? int update(int root, int pos, int val) { int newRoot = total++, tmp = newRoot; c[newRoot] = c[root] + val; int l = 1, r = m; while (l < r) { int mid = (l + r) >> 1; if (pos <= mid)//如果进入左子树, 那么右子树可以指向旧版本 { lson[newRoot] = total++; rson[newRoot] = rson[root]; newRoot = lson[newRoot]; root = lson[root]; r = mid; } else//左子树指向旧版本 { rson[newRoot] = total++; lson[newRoot] = lson[root]; newRoot = rson[newRoot]; root = rson[root]; l = mid + 1; } c[newRoot] = c[root] + val; } return tmp; } //每次插入都记录,它是插入左区间呢,还是右区间, 那么询问区间第k大时,只要该区间进入左区间的数字大于等于k,那么就去左子树找, //否则, 缩小k, 去右区间找 int query(int leftRoot, int rightRoot, int k) { int l = 1, r = m; while (l < r) { int mid = (l + r) >> 1; //c[lson[leftRoot]] - c[lson[rightRoot]] 表示区间内的数进入到左区间的有多少个 if (c[lson[leftRoot]] - c[lson[rightRoot]] >= k) { r = mid; leftRoot = lson[leftRoot]; rightRoot = lson[rightRoot]; } else { l = mid + 1; k -= c[lson[leftRoot]] - c[lson[rightRoot]]; leftRoot = rson[leftRoot]; rightRoot = rson[rightRoot]; } } return l; } int main() { int L, R, k; while (scanf("%d%d", &n, &q) == 2) { total = 0; for (int i = 1; i <= n; ++i) scanf("%d", &a[i]); initHash(); T[n + 1] = build(1, m); for (int i = n; i; i--) { int pos = Hash(a[i]); T[i] = update(T[i + 1], pos, 1); } while (q--) { scanf("%d%d%d", &L, &R, &k); printf("%d\n", t[query(T[L], T[R + 1], k)]); } } return 0; }