一句话算法:
主席树,又名可持久化线段树,是一种基于前缀和思想对历史版本进行保存,可支持单点修改,查询某个历史版本下某位置的值的数据结构。
主要思想:
对于我们上面加粗的命题,我们先考虑暴力的做法:
对于每一个历史状态,建立一棵线段树维护当前状态的信息。
然后你会发现,状态转移和空间耗损都非常巨大,(边(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
", b[res]);
}
return 0;
}