可持久化数据结构

可持久化的前提:本身拓扑结构不变

可持久化解决问题:记录数据结构的所有历史版本


核心思想:只记录每一个版本与上一个版本不一样的地方
如线段树:每次修改最多logn点

可持久化trie树

上一个版本的trie树种有这个字母的指针就直接copy过来,没有当前版本就新开一个指针。

数据最多 2 24 2^{24} 224 ,每次最多加 25 25 25 个点,所以开 25 25 25 倍数组

4.5X10^7个点
X4/10^6 = 180MB 

如果超过了题目所限我们可以根据题目给的空间反推我们能开多大

P4735 最大异或和
在这里插入图片描述

我们维护一个前缀异或和: s [ i ] = a [ 1 ]   x o r   a [ 2 ]   x o r   … a [ i − 1 ]   x o r   a [ i ] s[i] = a[1] \ xor\ a[2]\ xor\ … a[i-1] \ xor\ a[i] s[i]=a[1] xor a[2] xor a[i1] xor a[i]

a [ p ]   x o r   a [ p + 1 ]   x o r   …   x o r   a [ N ]   x o r   x a[p]\ xor\ a[p+1]\ xor\ …\ xor\ a[N]\ xor\ x a[p] xor a[p+1] xor  xor a[N] xor x 就相当于 s [ N ]   x o r   x   x o r   s [ p − 1 ] s[N] \ xor\ x\ xor\ s[p-1] s[N] xor x xor s[p1]

这样 S [ N ]   x o r   x S[N] \ xor\ x S[N] xor x就是一个定值 v a l val val,则相当于求一个 p p p 满足 l − 1 < = p − 1 < = r − 1 l-1 <= p-1 <= r-1 l1<=p1<=r1 使得 v a l   x o r   s [ p ] val \ xor\ s[p] val xor s[p] 值最大。

因为是异或运算,我们可以利用 “最大异或对” 这道题的一些性质,使用 t r i e trie trie 树。我们维护前缀异或和,找到一个 p p p ,贪心地使得 s [ p ] s[p] s[p]的二进制最高位开始到最低位的每一个数字都尽量与 v a l val val 的二进制对应位相反(相同为0不同为1)
前缀异或和的每个版本都可以用持久化的 t r i e trie trie 记录下来。
对持久化 t r i e trie trie 的每个节点额外维护一个信息 m a x _ i d max\_id max_id ,表示其所属的最大的持久化版本,显然一个节点的 m a x _ i d max\_id max_id 等于其子节点中最大的版本号(因为子节点要么是连向之前的版本,要么创建了该节点后再创建子节点)。

因为要满足边界性所以要用可持久化trie树的保存历史版本来界定边界

  • 边界 [ l − 1 , r − 1 ] [l-1, r-1] [l1,r1] 判断:

如果一个节点的 m a x _ i d max\_id max_id 小于 l − 1 l-1 l1 ,说明这个节点是 s [ l − 1 ] s[l-1] s[l1] 插入之前就已经创建出来的节点,不应该考虑在内。

对持久化树的某一个版本的根节点开始往下访问,所能访问到的节点的版本不会超过该根节点的版本,所以该题只需要从 r − 1 r-1 r1 版本开始访问即可满足取到的 p p p 小于等于 r − 1 r-1 r1

//trie树维护的是前缀异或和的二进制01串
#include
#include
#include
#include
using namespace std;
typedef long long ll;
const int N = 500007, M = N * 25;
/*
templateinline T read(T &x)
{
    x=0;ll f=1;char c;
    while(!isdigit(c=getchar()))if(c=='-')f=-1;
    while(isdigit(c)){x=(x<<1)+(x<<3)+(c^48);c=getchar();}
    return x*f;
}
*/
int n, m;
int sum[N];
int tr[M][2], max_id[M];
int root[N], idx;
//当前的区间位置(到时候[L,R]对比的就是这里的i)k表示当前是第几位,一共23位2^23,越高越大
void insert(int i, int k, int p, int q){//p上一个版本q下一个版本
    if(k < 0){
        max_id[q] = i;
        return ;
    }
    int v = sum[i] >> k & 1;
    if(p)//如果上个版本的当前结点是有东西的就继承一下
        tr[q][v ^ 1] = tr[p][v ^ 1];
    tr[q][v] = ++ idx;
    insert(i, k - 1, tr[p][v], tr[q][v]);
    max_id[q] = max(max_id[tr[q][0]], max_id[tr[q][1]]);
}

int query(int root, int C, int L){
    int p = root;
    for(int i = 23;i >= 0;i -- ){
        int v = C >> i & 1;
        //如果版本小于L-1,说明这个节点是s[l-1]插入之前就已经创建出来的节点,不应该考虑在内
        if(max_id[tr[p][v ^ 1]] >= L)//如果与当前这一位相反的数存在并且该数的位置在范围以内(求异或相反为1更大)
            p = tr[p][v ^ 1];
        else p = tr[p][v];
    }
    return C ^ sum[max_id[p]];
}

int main(){
    scanf("%d%d", &n, &m);
    max_id[0] = -1;//因为id从0开始这里要取一个更小的
    root[0] = ++ idx;
    insert(0, 23, 0, root[0]);

    for(int i = 1;i <= n; ++ i){
        int x;
        scanf("%d", &x);
        sum[i] = sum[i - 1] ^ x;//前缀异或和
        root[i] = ++ idx;
        insert(i, 23, root[i - 1], root[i]);//可持久化都是跟上一个版本比较

    }
    char op[2];

    int l, r, x;
    while(m -- ){
        scanf("%s", op);
        if(*op == 'A'){
            scanf("%d", &x);
            n ++ ;
            sum[n] = sum[n - 1] ^ x;
            root[n] = ++ idx;
            insert(n, 23, root[n - 1], root[n]);
        }
        else {
            scanf("%d%d%d", &l, &r, &x);
            //l<=p<=r,所以要查询r-1版本的
            printf("%d\n", query(root[r - 1], sum[n] ^ x, l - 1));
        }
    }
}

可持久化线段树

可持久化线段树,不能用堆的方法存子结点了,所以用指针l表示左儿子r表示右儿子

可持久化线段树难以处理区间修改,因为很难处理懒惰标记,除非使用永久化标记线段树

新版本其他的都不变,只把新的点替换,其他的性质例如左右儿子是谁不变

线段树维护的是值域
在数值上建立一个线段树,维护每个数值的区间上一共有几个数

线段树和平衡树都是二叉树,所以我们需要做二分的时候可以考虑能否在树里做二分

上一题是为知否存在这个数,比较特殊
但是本题不一样
本题是第R个版本个数cnt1减去第L-1个版本cnt2
L到R之间的个数为cnt1-cnt2

每次都是个上一个版本比较
l指的是左儿子,不是左边界

由于数据范围达到了1e9
所以需要离散化

1)静态求第k大数

学习笔记:可持久化线段树(主席树):静态 + 动态

你可能感兴趣的:(可持久化数据结构,#,主席树)