可持久化数据额结构--trie和线段树的可持久化版本。(两道模板题)

最近学啦这两种树的可持久化版本,有点感悟,记录一下。
两者都是动态开点建树。(’加加建树法‘)

可持久化trie

题目链接:
最大异或和
第一步::
这道题的解法需要一个前缀知识,是异或操作的可以像加减操作一样,也就是可以做一个前缀和数组,而且这道题目里面的需要的 left 到 right 区间里面选择一个p点之后,答案也就是可以转化成一个 S前缀数组里面的 S[N] ^ S[P-1] = A[p]^…A[N-1] ^A[N];最中需要求解的表达式也就转化成啦S[P-1] ^S[N] ^X;
然后因为需要必须选择一个节点所以是
printf("%d\n",query(root[r-1],s[n]^x,l-1));这条最后查询语句就是折磨写出来的,之前一个地方(root【r-1】)不理解,举个例子,就是当P指向N时,我们的表达式应该是S[N] ^S[N-1] ^X,所以这个从r-1的版本里面查找就说的过去啦。
第二步::
需要可持久化的trie树
trie树的可持久化建树方式和trie的建树方式是一样的,只不过对于根节点有多个版本,然后分支的话前后两种版本只有当前这个插入的数据这条路是不一样的,在每一个节点都有自己最后插入(或者遍历)的时间戳,在这个题目中是只有最后的时间 戳是需要记录的,个人理解:可能别的题目不知这一个时间戳。还可能是最先时间戳。
剩下的代码里面有部分备注,希望各位读者能读懂,,,写的不好勿喷。
当晚改啦一个地方,把零版本的0加入trie树是必要的,原因代码里面已经写啦,max_id[0]=-1这句话是为啦当我的版本节点不存在是,不会走进去。

#include <cstdio>
#include <cstring>
#include <algorithm>
#include <iostream>
using namespace std;
// const int N = 100010;
typedef long long ll;
typedef pair<int,int> PII;
#define x first
#define y second
const int N= 600010,M = N *25;
int n,m;
int s[N];
int tr[M][2],max_id[M];
int root[N],idx;
void insert(int i,int k,int p,int q)
{
    if(k<0)
    {
        max_id[q]=i;
        return;
    }
    int v=s[i]>>k&1;
    if(p)tr[q][v^1]=tr[p][v^1];//这里p为老版本,q为新版本,然后如果是不是需要更新的要直接借用老版本
    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]]);//相当于是pushup操作,借用分支节点的信息来更新当前节点的最后插入时间戳
}
int query(int root,int c, int limit)
{
    int p=root;
    for(int i=23;i>=0;i--)
    {
        int v=c>>i&1;
        if(max_id[tr[p][v^1]]>=limit)p=tr[p][v^1];//如果在比limit限制还要更新的版本,就可以往这条路上走。
        else p=tr[p][v];//否则就走和它相同的这条路,这样会将答案变小。
    }
    return c^s[max_id[p]];//c作为s[n]^x,这里和s里面的与其异或最远的max_i[dp]就是这个最大的以后最终值,这里理解有点跨度,建议回去先看这道题目的解题前置推导。
}
int main()
{
    scanf("%d%d",&n,&m);
    max_id[0]=-1;//这里赋值成-1是因为查询的时候需要r-1的版本,然后查询过程中,  有与limit比较的操作,所以需要将为开出的节点的记录赋值成-1,进不去。
    root[0]=++idx;
    insert(0,23,0,root[0]);//当找不到合适的值的时候需要(也就是当l-1,r-1都为0的时候)会走到不知道的地方,所以一开始加入0到trie树种可以到最后使其变成maxid=0;
    //这样最后查询的返回值为0,也就是异或值不变。保证不会有hack数据。
    for(int i=1;i<=n;i++)
    {
        int x;
        scanf("%d",&x);
        s[i]=s[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++;
            s[n]=s[n-1]^x;
            root[n]=++idx;
            insert(n,23,root[n-1],root[n]);
        }
        else 
        {
            scanf("%d%d%d",&l,&r,&x);
            printf("%d\n",query(root[r-1],s[n]^x,l-1));
        }
    }
    return 0;
}

可持久化线段树–主席树

学完第一感觉:这种树似乎是一个天然二分,加可持久化的trie的东西。

主席树的建树方式和上面提到的trie的建树方式是一样的,需要新加一个节点tot++,”加加“建树法。动态开点建树。
因为在第二个版本的根节点开始他已经不能满足
左子树:left = u << 1;
右子树:right= u <<1|1;
这种直接能得到左右子节点的函数。
所以为啦简化代码少些几个函数直接全部采用加加建树法还是比较好的。
现在感悟:trie和线段树都可以直接加加建树,直接舍弃那个乘二,和乘二加一的函数也不是不可以。
题目链接:
第k小的数
这道题目显然不能陪着题目开这么大的线段树,所以需要离散化,离散化之后去重,得到需要建出来的线段树。用lower_bound来直接得到下标作为离散化之后的值;
向上一版本上插入新的离散化后的值得到新的版本,在插入函数中,每一个新的路径得到的一条新链都是不能借用之前节点的(因为最下面的节点信息改变,pushuup上来时候里面的sum,cnt)统计的东西都会改变,所以需要新的节点来保存,也就继续加加建树。感觉代码里面inset函数的操作像是拿啦别人的东西一点没有给别人留下什么,自己把利益全都给吃掉啦,坏的鸭皮。新节点就是这种坏节点。
最后查询操作,就是将两种版本里面存的cnt差值找出来,如果left的差值已经够k个啦,那么最终答案一定是在左边的,否则网友查询。
代码:

#include <cstdio>
#include  <cstring>
#include <iostream>
#include <algorithm>
#include <vector>

using namespace std;

const int N = 100010,M = 10010;
int n,m;
int a[N];
vector<int> nums;
struct node
{
    int l,r;
    int cnt;
}tr[N*4+N*17];

int root[N],idx;

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;
}
int insert(int p,int l,int r,int x)
{
    int q=++idx;//q是新的版本节点。
    tr[q]=tr[p];//似乎是必然开一个新节点,与可持久化trie树有些不同。这一条新的链必然更新成新的东西。
    //新开的节点直接先继承上一版本的信息,然后再做更改。
    if(l==r)
    {
        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;//相当于pushup操作。
    return q;
}
int query(int q,int p,int l,int r, int k)//p为新版本,q为老版本
{
    if(l==r)return r;
    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);//向右边递归(因为查出的元素还不够,把左边减下去在继续从右边找);
}
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);//不像trie一样不用先建树,线段树先要建好这颗树。
    for(int i=1;i<=n;i++)
    {
        root[i]=insert(root[i-1],0,nums.size()-1,find(a[i]));//i-1相当于是老版本。
    }
    while(m--)
    {
        int l,r,k;
        scanf("%d%d%d",&l,&r,&k);
        printf("%d\n",nums[query(root[r],root[l-1],0,nums.size()-1,k)]);
        //查询0到nums. 里面这个区间的两种版本差值,里面还会有二分,直接解里面已经求好的和做一个二分就是能快速查询,

    }
    return 0;
}

终于淦完啦,理解就这些,欢迎留言讨论,我也是第一次学,没学那么透彻。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值