主席树(可持久化权值线段树)的相关知识
相关解释:
主席树的核心类似
g
i
t
操作,维护此时的树与前一个树的差异即可
主席树的核心类似git操作,维护此时的树与前一个树的差异即可
主席树的核心类似git操作,维护此时的树与前一个树的差异即可
即、每修改一个节点,我们就创建一个新的副本,并在副本的基础上进行修改
即、每修改一个节点,我们就创建一个新的副本,并在副本的基础上进行修改
即、每修改一个节点,我们就创建一个新的副本,并在副本的基础上进行修改
其中
,
每个版本我们都认为他是一颗线段树
其中, 每个版本我们都认为他是一颗线段树
其中,每个版本我们都认为他是一颗线段树
因为版本间只有若干节点的不同
因为版本间只有若干节点的不同
因为版本间只有若干节点的不同
因此其他相同节点是各个版本共用的
因此其他相同节点是各个版本共用的
因此其他相同节点是各个版本共用的
空间大小为:
4 ∗ N + 操作数 ∗ l o g N , 但保险起见一般开 N < < 5 (一般空间都可以满足的) , 并且一定不会少的 4 * N + 操作数 * logN, 但保险起见一般开 N << 5(一般空间都可以满足的), 并且一定不会少的 4∗N+操作数∗logN,但保险起见一般开N<<5(一般空间都可以满足的),并且一定不会少的
例题:
本题解法
(求区间第k小数)
数值上建立线段树
数值上建立线段树
数值上建立线段树
节点上建立前缀和
c
n
t
保存区间节点数量
节点上建立前缀和cnt保存区间节点数量
节点上建立前缀和cnt保存区间节点数量
将区间
[
L
,
R
]
的第
k
小数转化为“在第
r
版本和第
l
−
1
版本中二分查找节点数量恰好为
k
的版本”
将区间[L, R]的第k小数转化为“在第r版本和第l-1版本中二分查找节点数量恰好为k的版本”
将区间[L,R]的第k小数转化为“在第r版本和第l−1版本中二分查找节点数量恰好为k的版本”
具体实现:
- 1. 离散化 : 数值较大,节点数比较小 1.离散化: 数值较大,节点数比较小 1.离散化:数值较大,节点数比较小
-
2.
在数值上建立线段树
:
遍历区间的每一个元素,每遍历一个我们便创建一个新版本即
r
o
o
t
[
i
]
2.在数值上建立线段树: 遍历区间的每一个元素,每遍历一个我们便创建一个新版本即root[i]
2.在数值上建立线段树:遍历区间的每一个元素,每遍历一个我们便创建一个新版本即root[i]
这样我们则右 r o o t [ 0 − n ] 个版本的线段树 这样我们则右root[0-n]个版本的线段树 这样我们则右root[0−n]个版本的线段树 -
3.
用前缀和记录区间中的点的数量
:
询问
[
L
,
R
]
时,我们用
r
o
o
t
[
R
]
减去
r
o
o
t
[
L
−
1
]
,
3.用前缀和记录区间中的点的数量: 询问[L, R]时,我们用root[R]减去root[L - 1],
3.用前缀和记录区间中的点的数量:询问[L,R]时,我们用root[R]减去root[L−1],
如此即为 [ L , R ] 之间插入的数,即、第 L − 1 版本到第 R 版本之间改变了什么数 如此 即为[L,R]之间插入的数,即、第L-1版本到第R版本之间改变了什么数 如此即为[L,R]之间插入的数,即、第L−1版本到第R版本之间改变了什么数 - 4. 求区间 [ L , R ] = = 求版本 [ L , R ] 4.求区间[L,R] == 求版本[L,R] 4.求区间[L,R]==求版本[L,R]
- 5. 在 r o o t [ r ] 的版本和 r o o t [ l − 1 ] 的版本之间二分查找 c n t 5.在root[r]的版本和root[l-1]的版本之间二分查找cnt 5.在root[r]的版本和root[l−1]的版本之间二分查找cnt
步骤
5
举例为
:
步骤5举例为:
步骤5举例为:
代码如下:
#include <iostream>
#include <algorithm>
#include <cstring>
using namespace std;
const int N = 100010;
int n, m;
int a[N];
vector<int> nums;
struct Node
{
int l, r;
int cnt;
}tr[N << 5];
int root[N], idx; // root[i] 存每棵树的根节点的指针
// 离散化查找函数
int find(int x)
{
return lower_bound(nums.begin(), nums.end(), x) - nums.begin();
}
// 建立主席树,以指针形式连接左右儿子
int build(int l, int r)
{
int p = ++ idx;
if(l == r) return p;
int mid = l + r >> 1;
tr[p].l = build(l, mid), tr[p].r = build(mid + 1, r);
return p;
}
// 在[l, r]范围内查找x的位置,在x的位置上 cnt ++
int insert(int p, int l, int r, int x)
{
int q = ++ idx;
tr[q] = tr[p]; // 复制上一个版本
if(l == r) // 找到 cnt ++
{
tr[q].cnt ++;
return q;
}
int mid = l + r >> 1;
if(x <= mid) tr[q].l = insert(tr[p].l, l, mid, x); // 左子树查找
else tr[q].r = insert(tr[p].r, mid + 1, r, x); // 右子树查找
tr[q].cnt = tr[tr[q].l].cnt + tr[tr[q].r].cnt;
return q; // 返回此指针(idx)给 root[i]
}
// 在tr[q]与tr[p]的版本的差的新树中,找到第k小的数
// 即求出在[l, r]之间插入的数中的第k小数,
int query(int q, int p, int l, int r, int k)
{
if(l == r) return r;
// 查找第k小数,k可能 == 左子树的某一个数 或 右子树的某一个数的cnt + 左子树cnt之和
// 因此先计算左子树的cnt,
int cnt = tr[tr[q].l].cnt - tr[tr[p].l].cnt;
int mid = l + r >> 1;
if(k <= cnt) return query(tr[q].l, tr[p].l, l, mid, k); // 还在左边,继续递归左子树
else return query(tr[q].r, tr[p].r, mid + 1, r, k - cnt);
// 反之,递归右子树 - 左子树cnt之和
}
int main()
{
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i ++ )
{
scanf("%d", &a[i]);
nums.push_back({a[i]});
}
// 离散化
sort(nums.begin(), nums.end());
nums.erase(unique(nums.begin(), nums.end()), nums.end());
root[0] = build(0, nums.size() - 1);
// root[i]指向每个版本的树的根节点的指针
for(int i = 1; i <= n; i ++ )
root[i] = insert(root[i - 1], 0, nums.size() - 1, find(a[i]));
while(m -- )
{
int l, r, k;
scanf("%d%d%d", &l, &r, &k);
// 返回的为离散化的下标,加nums[i]取出真正的值
printf("%d\n", nums[query(root[r], root[l - 1], 0, nums.size() - 1, k)]);
}
return 0;
}