前置芝士
可持久化线段树
主席树的全称:可持久化权值线段树
可持久化
可持久化总是可以保留每一个历史版本,并且支持操作的不可变特性。
本质:多根线段树,每个根都代表一个历史版本。
线段树对于区间 [ 1 , n ] {[1,n]} [1,n] 的分解是唯一的, 所以 n {n} n 相同的线段树结构相同,这也是实现可持久化线段树的基础。
可持久化前提:本身的拓扑结构不变
详细图解
-
现在有一颗线段树,如下:
-
现赋权值如下:
-
红色节点为版本一(修改4处+1)后被更新的结果
:由上可以发现,对于一次操作,新版本改动的节点只有一条链
- 所以相对于上一版本,我们只需多记录一条链
:蓝色线为新版本(
r
o
o
t
[
1
]
{root[1]}
root[1])继承历史版本
r
o
o
t
[
0
]
{root[0]}
root[0] 的信息。
有了这些前置知识,主席树也不难理解了,接下来通过例题来逐步掌握主席树。
Acwing - 255. 第K小数
题目描述
输入格式
输出格式
样例
输入样例:
7 3
1 5 2 6 3 7 4
2 5 3
4 4 1
1 7 3
输出样例:
5
6
3
数据范围
N < = 1 0 5 , M < = 1 0 4 , ∣ A [ i ] ∣ < = 1 0 9 {N<=10^5,M<=10^4,|A[i]|<=10^9} N<=105,M<=104,∣A[i]∣<=109
思路
-
我们知道了对于一个序列的第k小,可以采用权值线段树。
- k < = c n t l e f t {k<=cnt_{left}} k<=cntleft,在左子树找第 k {k} k 小
- k > c n t l e f t {k>cnt_{left}} k>cntleft,在右子树找第 k − c n t l e f t {k-cnt_{left}} k−cntleft小
现在问题转换到任意区间
我们要用 [ l i , r i ] {[l_i,r_i]} [li,ri] 区间的数建立权值线段树。
发现可以使用前缀和来维护:
[ l i , r i ] {[l_i,r_i]} [li,ri] 等价于 分别以 [ 1 , l i ] {[1,l_i]} [1,li] 和 [ 1 , r i ] {[1,r_i]} [1,ri] 的数建立权值线段树,每个点的值对应位相减。
发现 [ 1 , i ] {[1,i]} [1,i] 和 [ 1 , i + 1 ] {[1,i+1]} [1,i+1] 区间内的数建立的权值线段树的差异仅仅是树中的一条链(包含 a [ i + 1 ] {a[i+1]} a[i+1]的区间次数+1,只会影响 l o g 2 n {log_2n} log2n个点)
那么考虑(动态开点),这样就可以预处理出 [ 1 , n ] {[1,n]} [1,n] 的所有权值线段树。
由于值域很大,所以进行离散化
代码
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 100010;
int n, m;
int a[N], b[N];
struct node {
int l, r;
int v;
} tr[N << 5];
int root[N], idx;
void pushup(int p) {
tr[p].v = tr[tr[p].l].v + tr[tr[p].r].v;
}
void build(int &p, int l, int r) {
p = ++idx;
if(l == r) return ;
int mid = l + r >> 1;
build(tr[p].l, l, mid), build(tr[p].r, mid + 1, r);
}
void modify(int &p, int q, int l, int r, int x, int v) {
p = ++idx;
tr[p] = tr[q];
if(l == r) { tr[p].v += v; return ; }
int mid = l + r >> 1;
if(x <= mid) modify(tr[p].l, tr[q].l, l, mid, x, v);
else modify(tr[p].r, tr[q].r, mid + 1, r, x, v);
pushup(p);
}
int query(int p, int q, int l, int r, int k) {
if(l == r) return r;
// 这是当前这段区间,r树-(l-1)树所维护的区间的左树值的个数
int cnt = tr[tr[p].l].v - tr[tr[q].l].v;
int mid = l + r >> 1;
if(k <= cnt) return query(tr[p].l, tr[q].l, l, mid, k);
return query(tr[p].r, tr[q].r, mid + 1, r, k - cnt);
}
int main()
{
cin >> n >> m;
for (int i = 1; i <= n; i++) cin >> a[i], b[i] = a[i];
sort(a + 1, a + 1 + n);
int len = unique(a + 1, a + 1 + n) - a - 1;
for (int i = 1; i <= n; i++)
b[i] = lower_bound(a + 1, a + 1 + len, b[i]) - a;
// 一开始先是建全树(所以用相对排名建树,而不是具体值域)
build(root[0], 1, len);
for (int i = 1; i <= n; i++)
modify(root[i], root[i - 1], 1, len, b[i], 1);
while(m--) {
int l, r, k;
cin >> l >> r >> k;
cout << a[query(root[r], root[l - 1], 1, len, k)] << endl;
}
return 0;
}