主席树入门

前言

可持久化线段树是动态开点的线段树,又被称为主席树。显然,听名字就知道,要想学习主席树,首先得学会线段树,如果还不会请戳这里,或者自己找一篇博客学习。
作为一种可持久化数据结构,主席树支持我们查询历史版本。具体的讲,就是如果你在原来的线段树上进行了一系列操作后,还要保存原有的以及每次操作后的线段树,这样才能查询历史版本。

正片

静态主席树

首先从我们的静态主席树开始入门。
下面以一个题为例,给你整明白主席树。

问题(点我进入

给你 N N N个数 a i a_i ai M M M次操作,现在有两种操作,①在某个历史版本上修改某一个位置上的值,②是查询某个历史版本上某个位置的值,其中 N , M ≤ 1 0 6 N,M\leq10^6 N,M106.
此外,每进行一次操作(对于操作②,即为生成一个完全一样的版本,不作任何改动),就会生成一个新的版本。版本编号即为当前操作的编号(从1开始编号,版本0表示初始状态数组)。

分析

线段树我们都会,如果要想维护 N N N个数的序列,那么他需要的空间为 4 N 4N 4N,如果我们还要可持久化,每次操作后都存一下,对于 M M M次操作,那么空间将会达到 4 M N 4MN 4MN,那么当 M M M很大的时候会非常耗内存,在算法竞赛中,这么大的空间一般是开不下的。那么有没有什么解决的办法呢?

当然是有的。首先我们得整明白,每一次操作之后,对于上一个状态的线段树而言,有多少个节点的状态发生了改变。这里由于我们是单点修改,所以其实只有一条链(从根节点开始一直到要修改的叶节点) l o g 2 N log_2N log2N个点发生了改变,而剩下的 4 N − l o g 2 N 4N-log_2N 4Nlog2N个点都没有改变,所以每次操作后,只需要记录改变的节点,其余没变的节点只需要原封不动继承就行。
下面一张图,是将长度为7的线段建成一棵线段树,并且改变 a 2 a_2 a2的值的图。
初始状态改变a2时修改的节点
操作后

这里的 r t [ i ] rt[i] rt[i]表示第 i i i次操作后线段树的根节点,其中 r t [ 0 ] rt[0] rt[0]是初始状态。

空间复杂度分析

初始状态 N N N个节点的线段树,需要的空间为 4 N 4N 4N,之后我们每次操作都会新增 l o g 2 N log_2N log2N个节点,
因此总体空间复杂度为 O ( 4 N + M l o g 2 N ) O(4N+Mlog_2N) O(4N+Mlog2N),此处 N , M N,M N,M都取一样的话就是 O ( ( 4 + l o g 2 N ) N ) O((4+log_2N)N) O((4+log2N)N),如果N有 1 0 6 10^6 106那开30倍的 N N N就够了。当然,如果你不放心可以再多开大些。

好了,讲了这么多,相信你已经理解主席树的思想——重用节点。那么要如何实现呢?因为他看起来好复杂。

实现

首先要解决记录问题:
(1)左/右子树节点
这里我们采用动态开点的方式,不能像原来的线段树(左/右子树节点为 2 u , 2 u + 1 2u,2u+1 2u,2u+1)那样直接确定左子树右子树节点。
(2)当前是哪个区间
这里我们可以在函数的里面记录
(3)叶节点的值
一个数组记就行

const int MAX = 1e6 + 10;//N, M
const int MAX_N = MAX * 30;//(logN + 4)倍, 可以再开大点

int rt[MAX], tot;//rt[i]为第i次操作后线段树的根节点, tot -> 动态开点
int lc[MAX_N], rc[MAX_N], val[MAX_N];//分别为左儿子, 右儿子, 

接下来看操作:
在这里只需要三个操作:
①建树
②单点修改
③区间查询

建树

和线段树类似,只是方式变为动态开点。

void build(int &u, int l, int r) {//这里用引用的方式
    u = ++tot;//给当前节点一个编号
    if (l == r) val[u] = a[l];//到达叶节点
    else {
        int mid = (l + r) / 2;
        build(lc[u], l, mid);//建左子树
        build(rc[u], mid + 1, r);//建右子树
    }
}

那么我们建树就只需要这样:

build(rt[0], 1, N);
单点修改
void update(int &u, int v, int l, int r, int p, int k) {//u为当前版本, v为过去的某个版本, p为修改的点, k为变成的值
    u = ++tot;//同理
    lc[u] = lc[v], rc[u] = rc[v];//首先, 当前版本要先继承过去版本的左右子树
    //然后在这个基础之上, 修改其中的一条链(就是找一个子树修改)
    if (l == r) val[u] = k;//到达叶节点修改即可
    else {
        int mid = (l + r) / 2;
        if (p <= mid) update(lc[u], lc[v], l, mid, p, k);//这样就是修改左子树,
        else update(rc[u], rc[v], mid + 1, r, p, k);//修改右子树
    }
}
单点查询
int query(int u, int l, int r, int p) {//这个比较简单, 就不解释了
    if (l == r) return val[u];
    else {
        int mid = (l + r) / 2;
        if (p <= mid) return query(lc[u], l, mid, p);
        else return query(rc[u], mid + 1, r, p);
    }
}
完整代码
#include<bits/stdc++.h>
using namespace std;
const int MAX = 1e6 + 10;
const int MAX_N = MAX * 30;

int N, M;
int a[MAX];

int rt[MAX], tot;
int lc[MAX_N], rc[MAX_N], val[MAX_N];

void build(int &u, int l, int r) {
    u = ++tot;
    if (l == r) val[u] = a[l];
    else {
        int mid = (l + r) / 2;
        build(lc[u], l, mid);
        build(rc[u], mid + 1, r);
    }
}

void update(int &u, int v, int l, int r, int p, int k) {
    u = ++tot;
    lc[u] = lc[v], rc[u] = rc[v];
    if (l == r) val[u] = k;
    else {
        int mid = (l + r) / 2;
        if (p <= mid) update(lc[u], lc[v], l, mid, p, k);
        else update(rc[u], rc[v], mid + 1, r, p, k);
    }
}

int query(int u, int l, int r, int p) {
    if (l == r) return val[u];
    else {
        int mid = (l + r) / 2;
        if (p <= mid) return query(lc[u], l, mid, p);
        else return query(rc[u], mid + 1, r, p);
    }
}

int main() {
    scanf("%d%d", &N, &M);
    for (int i = 1; i <= N; i++) scanf("%d", &a[i]);
    build(rt[0], 1, N);
    for (int i = 1; i <= M; i++) {
        int v, op, p; scanf("%d%d%d", &v, &op, &p);
        if (op == 1) {
            int k; scanf("%d", &k);
            update(rt[i], rt[v], 1, N, p, k);
        }
        else {
            printf("%d\n", query(rt[v], 1, N, p));
            rt[i] = rt[v];//题目说了查询后,生成一个一模一样的版本,那我们就将根节点复制就可以
        }
    }
    return 0;
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值