主席树
这篇题解很神!
P3919 【模板】可持久化线段树 1(可持久化数组) - 洛谷
https://www.cnblogs.com/zinthos/p/3899565.html
oi wiki
目标问题:
求区间第k大的值
思想:
维护历史版本的线段树,只复制修改的线段树节点就保存了历史的线段树,然后就建成了一颗以线段树为节点的主席树


主席树的性质:
1、每一次修改增加的节点个数为log(n)。
2、增加的非叶子结点会连向一个是其他版本的节点,一个是连向新节点。
3、主席树有很多根……
4、对于每一个根都可以构成一棵完整的线段树。
5、每一个节点都有可能有不只一个爸爸……
所以我们可以知道主席树只会对部分节点进行复制,并且每一次复制的节点个数是log(n)。我们每一次想询问一个版本的线段树,就可以在那个版本的根构成的线段树中询问。
1、怎么构建新节点?怎么给新节点编号?怎么连边?
2、怎么访问子节点?
3、怎么存根?
现在给出刚才问题的答案:
1、直接开一块内存池存新节点。编号为此时总节点数个数+1。开结构体存子节点编号;线段树建什么边,一指了事。
2、访问子节点编号,不是像线段树一样乘2或乘2+1,而是在结构体存子节点编号。
3、另外开个数组存。
代码:
struct kkk{
int l,r,val;
}tree[maxn];
//新建节点
int clone(int node){
top++;
tree[top]=tree[node];//全部信息都传到新节点
return top;
}
//建树就是新建主席树上的节点
int maketree(int node,int begin,int end){
node=++top;
if(begin==end){
tree[node].val=a[begin];
return top;
}
int mid=(begin+end)>>1;
tree[node].l=maketree(tree[node].l,begin,mid);
tree[node].r=maketree(tree[node].r,mid+1,end);
return node;
}
//更新
int update(int node,int begin,int end,int x,int val){
node=clone(node); //更新就要新建节点
if(begin==end){
tree[node].val=val;
}else{
int mid=(begin+end)>>1;
if(x<=mid)
tree[node].l=update(tree[node].l,begin,mid,x,val); //访问左子树
else
tree[node].r=update(tree[node].r,mid+1,end,x,val); //访问右子树
}
return node;
}
//询问
int query(int node,int begin,int end,int x){
if(begin==end){
return tree[node].val;
}else{
int mid=(begin+end)>>1;
if(x<=mid)
return query(tree[node].l,begin,mid,x); //访问左子树
else
return query(tree[node].r,mid+1,end,x); //访问右子树
}
}
//再来看主程序,里面看根怎么存储:
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;i++)scanf("%d",&a[i]);
root[0]=maketree(0,1,n); //root[i]为i版本的根编号,刚开始编号为0
for(int i=1;i<=m;i++){
scanf("%d%d%d",&rt,&mode,&x);
if(mode==1){
scanf("%d",&y);
root[i]=update(root[rt],1,n,x,y); //保存版本
}
else{
printf("%d\n",query(root[rt],1,n,x)); //输出
root[i]=root[rt]; //新建版本
}
}
}
【模板1】可持久化线段树1(可持久化数组)
代码:
/*
点修改 + 点查询
每次都新建一棵树(每次复制都只是复制修改的那条路径保证节省空间)
具体步骤:
复制 -> 更新 -> 递归-> 返回
可持久化数组的0版本:
[1..5]
/ \
[1..3] [4..5]
/ \ / \
[1..2] [3..3] [4..4] [5..5]
/ \ │ │ │
(59) (46) (14) (87) (91)
*/
#include <bits/stdc++.h>
using namespace std;
#define rep(i,a,b) for(int i = a; i <= b; i++)
const int N = 1e6 + 10, MAXN = N << 5;
int a[N], root[N], top; //主席树上的节点编号
struct Node{
int l, r, val;
}tree[MAXN];
//新建节点
int clone(int node){
top++;
tree[top] = tree[node]; // 复制一遍
return top; //返回当前新建的树的根节点
}
//建树就是新建主席树上的节点
int maketree(int node, int begin, int en){
node = ++top; // 新建节点就是建树
if(begin == en){
tree[node].val = a[begin];
return top;
}
int mid = (begin + en) >> 1;
tree[node].l = maketree(tree[node].l, begin, mid);
tree[node].r = maketree(tree[node].r, mid+1, en);
return node;
}
//修改,就是新建一棵树,然后只改变修改路径上的值(复制路径)
int update(int node, int begin, int en, int x, int val){
node = clone(node);
if(begin == en){
tree[node].val = val;
}else{
int mid = (begin+en) >> 1;
if(x <= mid) tree[node].l = update(tree[node].l, begin, mid, x, val);
else tree[node].r = update(tree[node].r, mid + 1, en, x, val);
}
return node;
}
//单点查询
int query(int node, int begin, int en, int x){
if(begin == en){
return tree[node].val;
}else{
int mid = (begin + en) >> 1;
if(x <= mid) return query(tree[node].l, begin, mid, x);
else return query(tree[node].r, mid + 1, en, x);
}
}
int main(){
ios::sync_with_stdio(0);cin.tie(0);
int n, m; cin >> n >> m;
rep(i, 1, n) cin >> a[i];
top = 0;
root[0] = maketree(0,1,n);
int ver, op, p;
rep(i, 1, m){
cin >> ver >> op >> p;
if(op == 1){
int x; cin >> x;
root[i] = update(root[ver], 1,n, p, x);
}else{
cout << query(root[ver], 1, n, p) << '\n';
root[i] = root[ver];
}
}
return 0;
}
【模板2】可持久化线段树2
代码:
/*
区间第k小:
主席树 = 离散化 + 权值线段树 + 前缀和思想
前缀【1,r】第k小:权值树上二分即可,k <= cntL,说明在左边有第k小,否则在右边找 k - cntL
区间【l,r】第k小:r版本的信息 - (l-1)版本的信息即可
每次插入都维护新的版本,维护出现值域出现次数就好了
举个例子:
离散化后的原始数组:
【3,1,2,4,5】
求解区间【2,4】第2小的过程:
1.建主席树:逐个离散化后插入权值线段树中,每次插入都新建一颗树
2.查询:2,4的第2小,本质就是就是在“频次差值树”上找第2小
【r - (l-1)】
cntL = tree[4].l.sum - tree[2].l.sum
//去左子树(小值域 <-> 第k小)看,sum维护次数,如果cnt >= k,说明有k个数字,可能存在第k小,否则,就去另外一边查找第k-cntL小
*/
#include <bits/stdc++.h>
using namespace std;
const int MAXN = 200000 + 5;
// 最坏节点数 ≈ (N + N·logN) * 2 ≈ N*25
const int MAXNODE = MAXN * 25;
int n, q;
int a[MAXN], b[MAXN], m;
int root[MAXN], cntNode;
// 一个节点封装了 左/右 子指针 + 该区间频次和
struct Node {
int l, r, sum;
} tree[MAXNODE];
// build:建一棵空的值域树,返回根节点编号
int build(int L, int R) {
int u = ++cntNode;
tree[u].sum = 0;
if (L < R) {
int M = (L + R) >> 1;
tree[u].l = build(L, M);
tree[u].r = build(M+1, R);
}
return u;
}
// update:在旧版本 pre 的基础上,在位置 x 处 +1,并返回新根
int update(int pre, int L, int R, int x) {
int u = ++cntNode;
tree[u] = tree[pre]; // 路径复制:先整体拷贝
tree[u].sum++; // 累加频次
if (L < R) {
int M = (L + R) >> 1;
if (x <= M)
tree[u].l = update(tree[pre].l, L, M, x);
else
tree[u].r = update(tree[pre].r, M+1, R, x);
}
return u;
}
// query:在版本 u->v 的差上找第 k 小(值域 [L..R])
int query(int u, int v, int L, int R, int k) {
if (L == R) return L;
int M = (L + R) >> 1;
int cntLeft = tree[ tree[v].l ].sum - tree[ tree[u].l ].sum;
if (cntLeft >= k)
return query(tree[u].l, tree[v].l, L, M, k);
else
return query(tree[u].r, tree[v].r, M+1, R, k - cntLeft);
}
int main(){
ios::sync_with_stdio(false);
cin.tie(nullptr);
cin >> n >> q;
// 读数组并做离散
for (int i = 1; i <= n; i++) {
cin >> a[i];
b[i] = a[i];
}
sort(b+1, b+1+n);
m = unique(b+1, b+1+n) - (b+1);
// 版本 0:空树
cntNode = 0;
root[0] = build(1, m);
// 构造前缀版本:每插入一个 a[i] 就 update 一次
for (int i = 1; i <= n; i++) {
int t = int(lower_bound(b+1, b+1+m, a[i]) - b);
root[i] = update(root[i-1], 1, m, t);
}
// 处理 q 次区间第 k 小查询
while (q--) {
int l, r, k;
cin >> l >> r >> k;
int idx = query(root[l-1], root[r], 1, m, k);
cout << b[idx] << "\n";
}
return 0;
}
日常水话:
明天又有考试了,究竟是什么让我能花时间来学习这个主席树模板?
我又为什么能花这么就得时间来学这个?还真就给我搞懂了~
这个瘾是真的大啊,网瘾少年该戒了
提一嘴杭电补题工程得问题,因为这周考试,所以需要加紧复习,但是为了保证明天的题目量与学校课程的推进,
最后只在5天内学了2个算法,还只是停留在模板题目阶段(能弄明白已经很艰辛了QWQ)
所以,可能就这么荒废了?后面来补起来吧,蓝桥杯也就那样(真想骂人)还是icpc好玩,争取进校队下半年拿牌!
好了,就这样了,还要复习考试www,run~

浙公网安备 33010602011771号