可持久化数据结构学习笔记

/*
可持久化的迹象,我们俯身欣赏!
                  ——《膜你抄》
*/   

引子

我们在生活中可能会遇到这样的问题,要是某一变化是基于某一个历史版本而来的变化。

这样处理的过程就比较困难。(然而对于暴力这个一点都不困难)

有什么是暴力算法解决不了的呢?

又有什么暴力算法是优化不了的呢?

我们分析暴力算法的复杂度(裸暴力我们就不说了)

考虑有点技术含量的暴力:我开M个数据结构维护每一时刻的版本然后在那一时刻的版本上做操作。

Hmm...复杂度是对了,但是空间呢?大概联赛给你开放整一个内存吧。(可能还不够。。。)

我们分析这样的劣势:就是把以前一样的东西重复记录的M次

要是我们只改变有修改部分的结构就好了!!!

于是就有了可持久化数据结构:我们只记录原来数据结构中发生改变的副本,其他的留在原数据结构中。

从Trie树开始的可持久化之旅

这个首先得搞懂思想(请确保读懂上面相关知识)

扫描二维码关注公众号,回复: 5150440 查看本文章

在可持久化Trie树中插入一个元素的步骤一般如下:

  1. 设当前可持久化Trie树的根为lstrt(lastroot),插入元素以后的根为nowrt(nowroot)
  2. 建立一个新的节点(根)NowRoot
  3. 把nowrt下所有元素的指针所指信息置为lstrt中的所有指针信息(就是吧trie[nowrt][s]=trie[lstrt][s],s为字符所有可能,但是要除去当前字符信息)
  4. 对于当前字符信息重新维护,并新建节点new,trie[nowrt][now_char]=new(当前字符信息重新指)
  5. 令lstrt=trie[lstrt][now_char],nowrt=trie[nowrt][new_char] (重新准备迭代)
  6. 重复3-5至所有的new_char处理完毕。

这里是对于四个字符串依次插入可持久化Trie树的图,结合上述思想理解一下:

这四个字符串依次是:“abc","abd","abcd","bcd”.

这里是一个例题:P4735 最大异或和

Solution:显然需要考虑前缀xor和,记为s[i]

对于询问操作 l,r , x 就是询问一个位子p∈[l-1,r-1]使s[p] xor (x xor s[n])

如果只考虑右端点限制,那么就是直接从root[r-1] 访问进去贪心选取就行,

我们再次考虑左端点的限制,那么我们记lastest[x]表示指针所指元素位子为x时,最后面元素插入时经过这个点的最大的序号。

(如果没有元素经过这个点置为最小值-1:我们坚决不访问他)

然后询问时候考虑左端点限制是在处理下一个点去哪的位置是last值必须大于等于l-1才被认为有效点,

然后剩下的贪心(走和当前位相反或者走有元素那边)就行。

Hint:本题卡常#3和#6点建议打上O2优化、快读、还有别打那么多头文件...

# pragma G++ optimize(2)
# include <cstdio>
using namespace std;
const int N=600010;
int n,m,tot;
int trie[N*24][2],lastest[N*24],root[N],s[N];
inline int max(int x,int y){return (x>y)?x:y;}
inline int read() {
    static char c= getchar();
    int a= 0;
    while(c < '0' || c > '9') c= getchar();
    while(c >= '0' && c <= '9') a= a * 10 + c - '0', c= getchar();
    return a;
}
void insert(int i,int dep,int lstrt,int nowrt)
{
    if (dep<0) { lastest[nowrt]=i; return;}
    int c=(s[i]>>dep) & 1;
    if (lstrt!=0) trie[nowrt][c^1]=trie[lstrt][c^1];
    trie[nowrt][c]=++tot;
    insert(i,dep-1,trie[lstrt][c],trie[nowrt][c]);
    lastest[nowrt]=max(lastest[trie[nowrt][0]],lastest[trie[nowrt][1]]);
}
int ask(int nowrt,int val,int dep,int mark)
{
    if (dep<0) return s[lastest[nowrt]]^val;
    int c=(val>>dep) & 1;
    if (lastest[trie[nowrt][c^1]]>=mark)
        return ask(trie[nowrt][c^1],val,dep-1,mark);
    else
        return ask(trie[nowrt][c],val,dep-1,mark);
}
inline void write(int x)
{
    if (x>9) write(x/10);
    putchar('0'+x%10);
}
int main()
{
    n=read();m=read();
     int t;
     lastest[0]=-1; root[0]=++tot;
    insert(0,23,0,root[0]);
    for (int i=1;i<=n;i++) {
        t=read(); s[i]=s[i-1]^t;
        root[i]=++tot;
        insert(i,23,root[i-1],root[i]);
    }
    char op[3];
    for (int i=1;i<=m;i++) {
        scanf("%s",op);
        if (op[0]=='A') {
            int x=read();
            root[++n]=++tot;
            s[n]=s[n-1]^x;
            insert(n,23,root[n-1],root[n]);
        } else {
            int l=read(),r=read(),x=read();
            write(ask(root[r-1],x^s[n],23,l-1));
            putchar('\n');
        }
    }
    return 0;
}
View Code

提高:可持久化 SegmentTree

例题1: 【模板】可持久化数组(可持久化线段树/平衡树)

这个题目其实就是可持久化思想的运用,我这里写了一个维护区间的max(什么都不维护感觉慎得慌)

我把它改了一下不影响题意!!!

/*
对于操作1:输入v,1,pos,val 在历史版本v中把pos位置的数改为val作为当前版本
对于操作2:输入v,2,l,在历史版本v中输出第l位置和第l位置之间的所有数的最大值,并把版本v作为当前版本
*/

考虑怎么建立一棵可持久化SegmentTree,还是保留上面可持久化的思想,只维护有更改的那些线段。

其他的不做更改(直接链到对应节点就行)。

如图:

其实这个思想和前面的思想很像,但是此处和常规的线段树不同,他没有树形结构!!所以要数组模拟链表。

我们在每一个线段树的节点记录lc和rc作为左儿子节点编号和右儿子节点的编号,按照开节点的顺序编号就行。

为了节省空间,儿子的编号作为递归参数传递!

//建树
int build(int l,int r)
{
    int p=++tot;
    if (l==r) { tree[p].val=a[l]; return p;}
    int mid=(l+r)>>1;
    tree[p].lc=build(l,mid);
    tree[p].rc=build(mid+1,r);
    tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val);
    return p;
}

建树的过程也不用赘述了吧。

然后是更改insert(now,l,r,x,val)操作表示当前在编号为now节点,当前区间为[l,r],然后吧x位置的值单点修改为val(即a[x]=val)

//更改
int insert(int now,int l,int r,int x,int val)
{
    int p=++tot;
    tree[p]=tree[now];
    if (l==r) { tree[p].val=val; return p;}
    int mid=(l+r)>>1;
    if (x<=mid) tree[p].lc=insert(tree[now].lc,l,mid,x,val);
    else        tree[p].rc=insert(tree[now].rc,mid+1,r,x,val);
    tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val);
    return p;
}

注意到有个地方容易码错就是在递归insert的时候是从tree[now]出发而不是tree[p](否则不是自己到自己了吗)

但是不知道怎么过的三个点orz

接下来是query函数(这个和普通线段树大同小异)

//询问
int query(int now,int l,int r,int opl,int opr)
{
    if (opl<=l&&r<=opr) return tree[now].val;
    int mid=(l+r)>>1;
    int val=-inf;
    if (opl<=mid) val=max(val,query(tree[now].lc,l,mid,opl,opr));
    if (opr>mid)  val=max(val,query(tree[now].rc,mid+1,r,opl,opr));
    return val;
}

鼓掌~~(就这么码完了)

(注意下:main函数调用子函数,and 版本编号初始为0,后面第i个询问都有基于前的新版本)

// luogu-judger-enable-o2
# include <cstdio>
# define inf (0x7f7f7f7f)
# define int long long
using namespace std;
const int N=1e6+10;
int n,m,tot;
int root[N*20],a[N];
struct Sem_Tree{
    int lc,rc,val;
}tree[N*20];
int max(int x,int y){return (x>y)?x:y;}
inline int read()
{
    int X=0,w=0; char c=0;
    while(c<'0'||c>'9') {w|=c=='-';c=getchar();}
    while(c>='0'&&c<='9') X=(X<<3)+(X<<1)+(c^48),c=getchar();
    return w?-X:X;
}
void write(int x)
{
    if (x<0) putchar('-'),x=-x;
    if (x>9) write(x/10);
    putchar('0'+x%10);
}
int build(int l,int r)
{
    int p=++tot;
    if (l==r) { tree[p].val=a[l]; return p;}
    int mid=(l+r)>>1;
    tree[p].lc=build(l,mid);
    tree[p].rc=build(mid+1,r);
    tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val);
    return p;
}
int insert(int now,int l,int r,int x,int val)
{
    int p=++tot;
    tree[p]=tree[now];
    if (l==r) { tree[p].val=val; return p;}
    int mid=(l+r)>>1;
    if (x<=mid) tree[p].lc=insert(tree[now].lc,l,mid,x,val);
    else         tree[p].rc=insert(tree[now].rc,mid+1,r,x,val);
    tree[p].val=max(tree[tree[p].lc].val,tree[tree[p].rc].val);
    return p;
}
int query(int now,int l,int r,int opl,int opr)
{
    if (opl<=l&&r<=opr) return tree[now].val;
    int mid=(l+r)>>1;
    int val=-inf;
    if (opl<=mid) val=max(val,query(tree[now].lc,l,mid,opl,opr));
    if (opr>mid)  val=max(val,query(tree[now].rc,mid+1,r,opl,opr));
    return val;
}
signed main()
{
    n=read();m=read();
    for (int i=1;i<=n;i++) a[i]=read();
    root[0]=build(1,n);
    for (int i=1;i<=m;i++){
        int id=read(),op=read();
        if (op==1) {
            int pos=read(),v=read();
            root[i]=insert(root[id],1,n,pos,v);
        } else {
            int pos=read();
            int ans=query(root[id],1,n,pos,pos);
            root[i]=root[id];
            write(ans);putchar('\n');
        }
    }
    return 0;
}
View Code

后面还有第二个稍微难一点的例题(区间第K小数)

猜你喜欢

转载自www.cnblogs.com/ljc20020730/p/10353707.html