可持久化线段树(主席树)

引入

我们先来看一道题:

  • 给定 n n n 个数,一共 m m m 次询问,每次都要询问区间 [ l , r ] [l,r] [l,r] 的第 k k k 大的数。 其中 n , m , l , r , k n,m,l,r,k n,m,l,r,k 均不超过 2 ⋅ 1 0 5 2\cdot 10^5 2105,保证询问有答案。

我们会发现如果我们用普通的线段树好像解决不了这样的问题,这时候我们就需要新的做法,也就是棵持久化线段树(主席树)。

思路

我们知道我们每次插入线段树也都代表了一次历史记录,我们来考虑如何存储这种历史记录。

最暴力的做法就是每次我们都新开一棵线段树,用来存储到目前为止插入的所有节点,很明显,这个空间明显存不下,我们来想想如何优化空间。

我们会发现再新建一棵线段树的时候,会有许多节点根上一棵线段树是一模一样的(具体点说,我们每次插入都只会改变 l o g 2 ( n ) log_2(n) log2(n) 个节点),这时候我们就不需要在新开点记录这些一模一样点的信息,可以直接引用这些点,没必要为其新开点。

当我们插入一个点时,我们就新开一个线段树,然后在这个线段树上插入这个数,插入完后这个线段树其实就是一条链,我们再插入的时候就可以判断,如果我们插入的这个树是当前节点的左/右儿子,那我们就将当前节点的右/左儿子设为上一棵线段树相同值域节点的右/左儿子。

我们查询的时候就可以按照普通线段树来查询,还要记录你在第 i i i 次插入线段树时的节点是哪一个节点。

代码


//rt1就是你新插入的线段树的当前节点,rt2就是上一棵线段树的当前节点
void insert(int &rt1, int rt2, int l, int r, int val) {
    rt1 = ++cnt;//我们因为改变了当前节点,就为他新建一个节点来存储当前的信息。
    tree[rt1] = tree[rt2];//然后我们可以先将我们当前左右儿子的信息设为上一棵线段树的,再枚举时在更改
    if (l == r) {//如果枚举到了叶节点
        tree[rt1].sz++;
        return ;
    }
    int mid = (l + r) / 2;
    if (val <= mid)//如果我们要改变的是当前节点的左儿子,就去枚举并改变它
        insert(tree[rt1].lson, tree[rt2].lson, l, mid, val);
    else
        insert(tree[rt1].rson, tree[rt2].rson, mid + 1, r, val);
    updata(rt1);//记住一定要记得更新
}

例题

  • 【模板】可持久化线段树 2:洛谷

你可能感兴趣的:(数据结构,c++,树,可持久化线段树)