关于可持久化数据结构的简单理解~

动态开点线段树

在讨论可持久化线段树之前,我们先来讨论一下动态开点的线段树,当我们需要建多棵树存储状态而且最终需要的状态并不是完全的时候,为了避免MLE,我们就需要动态开点的线段树,为什么可以动态开点呢,因为我们知道,每次对于线段树的单点更新操作,之会影响到一条链上的节点信息,所以我们只要将这一条链上的与更新信息有关的子树信息进行更新,其他的直接继承之前的信息,这样每次最多更新log个节点,总空间复杂度最多nlogn
题目链接
http://acm.hdu.edu.cn/showproblem.php?pid=6183
题意
给 你 一 个 二 维 平 面 , 最 开 始 平 面 上 没 有 任 何 点 , 有 4 种 操 作 , 给你一个二维平面,最开始平面上没有任何点,有4种操作, 4
第 一 种 操 作 是   0   表 示 清 除 这 个 平 面 上 所 有 的 点 第一种操作是 \ 0 \ 表示清除这个平面上所有的点  0 
第 二 种 操 作 是 1   x   y   c 表 示 在 ( x , y ) 上 添 加 一 个 颜 色 为 c 的 点 第二种操作是1\ x \ y\ c 表示在(x,y)上添加一个颜色为c的点 1 x y c(x,y)c
第 三 种 操 作 是 2   x   y 1   y 2 查 询 ( 1 , y 1 ) , ( x , y 2 ) 之 间 有 多 少 种 颜 色 第三种操作是2\ x \ y_1 \ y_2 查询(1,y1),(x,y2)之间有多少种颜色 2 x y1 y2(1,y1),(x,y2)
第 四 种 操 作 是 3   表 示 直 接 退 出 程 序 第四种操作是3\ 表示直接退出程序 3 退
下 标 范 围 1 0 6 , 最 多 有 50 种 颜 色 下标范围10^6,最多有50种颜色 106,50
1 , 2 操 作 最 多 1.5 ∗ 1 0 5 次 1,2操作最多1.5*10^5次 121.5105
做法
我 们 想 想 只 有 一 种 颜 色 怎 么 办 , 由 于 左 界 限 已 经 固 定 我们想想只有一种颜色怎么办,由于左界限已经固定
这 就 意 味 着 我 们 可 以 对 y 建 立 权 值 线 段 树 这就意味着我们可以对y建立权值线段树 y线
更 新 操 作 只 要 保 留 每 个 y 位 置 最 小 的 x 就 可 以 更新操作只要保留每个y位置最小的x就可以 yx
这 样 查 询 的 时 候 只 要 看 这 个 颜 色 在 ( y 1 , y 2 ) 这 个 区 间 的 最 小 值 是 不 是 小 于 x 这样查询的时候只要看这个颜色在(y1,y2)这个区间的最小值是不是小于x (y1,y2)x
小 于 则 代 表 这 个 颜 色 在 这 个 范 围 内 出 现 过 小于则代表这个颜色在这个范围内出现过
但 是 本 题 有 50 种 颜 色 , 这 就 意 味 着 我 们 可 以 建 50 颗 线 段 树 分 别 计 算 但是本题有50种颜色,这就意味着我们可以建50颗线段树分别计算 5050线
但 是 这 个 数 据 范 围 明 显 会 M L E , 这 时 候 我 们 就 需 要 动 态 开 点 的 线 段 树 但是这个数据范围明显会MLE,这时候我们就需要动态开点的线段树 MLE线
每 次 更 新 一 条 链 上 的 信 息 , 返 回 这 个 颜 色 对 应 的 新 的 根 节 点 下 标 每次更新一条链上的信息,返回这个颜色对应的新的根节点下标
这 样 每 次 更 新 操 作 只 会 产 生 l o g 个 新 节 点 , 就 没 有 M L E 的 问 题 这样每次更新操作只会产生log个新节点,就没有MLE的问题 logMLE
注 意 一 定 要 先 继 承 之 前 的 信 息 , 再 更 新 本 次 更 新 的 信 息 , 保 证 之 前 的 信 息 被 继 承 下 来 。 注意一定要先继承之前的信息,再更新本次更新的信息,保证之前的信息被继承下来。
代码

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int INF = 0x3f3f3f3f;
const int maxn = 1e6+10;
int tot,minn[maxn*4],lson[maxn*4],rson[maxn*4],root[55];
void init()
{
    tot=0;
    memset(minn,INF,sizeof(minn));
    memset(lson,0,sizeof(lson));
    memset(rson,0,sizeof(rson));
    memset(root,0,sizeof(root));
}
void update(int &now,int l,int r,int pos,int val)
{
    if(!now)
    {
        now=++tot;
        minn[now]=val;
    }
    if(l==r)
    {
        minn[now]=min(minn[now],val);
        return ;
    }
    int mid=(l+r)>>1;
    if(pos<=mid) update(lson[now],l,mid,pos,val);
    else update(rson[now],mid+1,r,pos,val);
    minn[now]=min(minn[lson[now]],minn[rson[now]]);
    return ;
}
int query(int now,int ql,int qr,int l,int r,int maxx)
{
    if(!now) return 0;
    if(ql<=l&&qr>=r)
    {
        if(minn[now]<=maxx) return 1;
        else return 0;
    }
    int mid=l+r>>1;
    int ans=0;
    if(ql<=mid) ans=query(lson[now],ql,qr,l,mid,maxx);
    if(ans>0) return 1;
    if(qr>mid) ans=query(rson[now],ql,qr,mid+1,r,maxx);
    if(ans>0) return 1;
    else return 0;
}
int main()
{
    int op,x,y,c,y1,y2;
    while(scanf("%d",&op)!=EOF)
    {
        if(op==0)
        {
            init();
        }
        else if(op==1)
        {
            scanf("%d%d%d",&x,&y,&c);
            update(root[c],1,1000000,y,x);
        }
        else if(op==2)
        {
            scanf("%d%d%d",&x,&y1,&y2);
            int ans=0;
            for(int i=0;i<=50;i++)
            {
                ans+=query(root[i],y1,y2,1,1000000,x);
            }
            printf("%d\n",ans);
        }
        else
        {
            break;
        }
    }
    return 0;
}


可持久化线段树

对于之前的动态开点线段树理解之后,可持久化线段树就非常好理解了,这就是一个权值线段树的前缀和操作,我们对每个原数组下标为新的根节点去更新权值线段树的信息时,就可以得到当前节点之前所表示的权值线段树的信息,再用前缀和的思想对某个区间内的权值线段树查询,就可以得到某个区间内的权值线段树的信息,也就是主席树。
题目链接
http://acm.hdu.edu.cn/showproblem.php?pid=2665
题意
区 间 第 k 小 区间第k小 k
做法
如 果 给 你 某 个 完 整 序 列 的 权 值 线 段 树 信 息 如果给你某个完整序列的权值线段树信息 线
并 求 整 个 序 列 的 第 k 小 并求整个序列的第k小 k
我 们 就 可 以 直 接 在 权 值 线 段 树 上 进 行 查 询 我们就可以直接在权值线段树上进行查询 线
权 值 线 段 树 每 个 节 点 存 储 当 前 区 间 点 的 个 数 和 权值线段树每个节点存储当前区间点的个数和 线
每 次 访 问 一 个 节 点 , 看 这 个 节 点 的 左 子 树 的 节 点 值 是 否 大 于 等 于 k 每次访问一个节点,看这个节点的左子树的节点值是否大于等于k 访k
若 大 于 k 则 表 示 区 间 第 k 小 一 定 在 左 子 树 若大于k则表示区间第k小一定在左子树 kk
否 则 我 们 设 左 子 树 节 点 值 为 t m p , 由 于 t m p &lt; k , 所 以 我 们 只 要 在 右 子 树 种 查 询 区 间 第 ( k − t m p ) 小 就 可 以 了 否则我们设左子树节点值为tmp,由于tmp&lt;k,所以我们只要在右子树种查询区间第(k-tmp)小就可以了 tmp,tmp<k,(ktmp)
代码

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
const int maxn = 1e5+3;
int lson[maxn*20],rson[maxn*20],tot,sum[maxn*20],root[maxn];
int a[maxn],Hash[maxn],b[maxn];
void update(int &now,int pre,int l,int r,int pos)
{
    now=++tot;
    sum[now]=sum[pre]+1;
    lson[now]=lson[pre];
    rson[now]=rson[pre];
    if(l==r)    return ;
    int mid=l+r>>1;
    if(pos<=mid) update(lson[now],lson[pre],l,mid,pos);
    else update(rson[now],rson[pre],mid+1,r,pos);
}
int query(int st,int en,int l,int r,int k)
{
    if(l==r) return l;
    int tmp=sum[lson[en]]-sum[lson[st]];
    int mid=l+r>>1;
    if(tmp>=k) return query(lson[st],lson[en],l,mid,k);
    else return query(rson[st],rson[en],mid+1,r,k-tmp);
}
int main()
{
    int t;
    scanf("%d",&t);
    while(t--)
    {
        int n,q,x,y,k;
        tot=0;//0为根的树所有信息一直是0,每次得到第一棵树的信息都是从第0颗树传递,所以只重置tot即可,其他都不需要重置
        scanf("%d%d",&n,&q);
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&a[i]);
            Hash[i]=a[i];
        }
        sort(Hash+1,Hash+1+n);
        int d=unique(Hash+1,Hash+1+n)-Hash-1;
        for(int i=1;i<=n;i++)
        {
            int pos=lower_bound(Hash+1,Hash+1+d,a[i])-Hash;
            b[pos]=a[i];
            update(root[i],root[i-1],1,d,pos);
        }
        while(q--)
        {
            scanf("%d%d%d",&x,&y,&k);
            printf("%d\n",b[query(root[x-1],root[y],1,d,k)]);
        }
    }
    return 0;
}

可持久化01字典树

讨论可持久化01字典树之前,我们先来想一下01字典树的插入操作,插入操作就是从高位向低位去遍历,若当前包含本位则继续递归右儿子,否则递归左儿子,我们发现每次更新的信息还是一条链上的信息,所以如果我们想得到某个区间上的01字典树信息,我们也可以向可持久化线段树一样保存一个前缀和版本的01字典树信息,就可以了。
题目链接
http://acm.hdu.edu.cn/showproblem.php?pid=6191
题意
给 你 一 颗 带 权 树 , 每 次 查 询 以 u 为 根 的 子 树 上 与 x 异 或 的 最 大 值 给你一颗带权树,每次查询以u为根的子树上与x异或的最大值 ux
做法
由 于 出 现 子 树 , 我 们 就 可 以 处 理 出 d f s 序 由于出现子树,我们就可以处理出dfs序 dfs
之 后 在 d f s 序 上 按 顺 序 建 立 可 持 久 化 01 字 典 树 之后在dfs序上按顺序建立可持久化01字典树 dfs01
之 后 就 类 似 主 席 树 用 前 缀 和 操 作 来 获 取 某 个 区 间 的 01 字 典 树 信 息 之后就类似主席树用前缀和操作来获取某个区间的01字典树信息 01
进 行 正 常 的 01 字 典 树 查 询 异 或 最 大 值 就 可 以 了 进行正常的01字典树查询异或最大值就可以了 01
代码

#include<stdio.h>
#include<iostream>
#include<algorithm>
#include<string.h>
using namespace std;
typedef long long ll;
#define dbg(x) cout<<#x<<" = "<<x<<endl
const int Mod= 1000000007;
const int maxn=2e5+5;
int fa[maxn];
int val[maxn];
struct node
{
    int to,nxt;
}edge[maxn];
int head[maxn],tot;
int in[maxn],out[maxn];
int num[maxn];
int coun;
void addedge(int u,int v)
{
    edge[tot].to=v;
    edge[tot].nxt=head[u];
    head[u]=tot++;
}
void dfs(int u)
{
    in[u]=++coun;
    num[coun]=u;
    for(int i=head[u];i+1;i=edge[i].nxt)
    {
        dfs(edge[i].to);
    }
    out[u]=coun;
}
int tt,root[maxn],son[maxn*35][2],sum[maxn*35];
bool bs[35];
void insert_(int pos,int pre,int &now)
{
    now=++tot;
    son[now][0]=son[pre][0],son[now][1]=son[pre][1];
    sum[now]=sum[pre]+1;
    if(pos==0) return ;
    insert_(pos-1,son[pre][bs[pos-1]],son[now][bs[pos-1]]);
}
int query(int pos,int st,int en)
{
    if(pos==0) return 0;
    int tmp=sum[son[en][bs[pos-1]]]-sum[son[st][bs[pos-1]]];
    if(tmp>0) return query(pos-1,son[st][bs[pos-1]],son[en][bs[pos-1]])+(1<<(pos-1));
    else return query(pos-1,son[st][bs[pos-1]^1],son[en][bs[pos-1]^1]);
}
int main()
{
   int n,q,u,x;
   while(scanf("%d%d",&n,&q)!=EOF)
   {
       memset(head,-1,sizeof(head));
       memset(sum,0,sizeof(sum));
       tot=coun=0;
       tt=1;
       for(int i=1;i<=n;i++)   scanf("%d",&val[i]);
       for(int i=2;i<=n;i++)
        {
            scanf("%d",&x);
            addedge(x,i);
        }
        dfs(1);
        for(int i=1;i<=n;i++)
        {
            int pos=num[i];
            for(int j=0;j<30;j++)
            {
                bs[j]=1&(val[pos]>>j);
            }
            insert_(30,root[i-1],root[i]);
        }
        for(int i=1;i<=q;i++)
        {
            scanf("%d%d",&u,&x);
            for(int j=0;j<30;j++)
            {
                bs[j]=!(1&(x>>j));
            }
            int l=in[u],r=out[u];
            printf("%d\n",query(30,root[l-1],root[r]));
        }
   }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值