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

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

引子

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

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

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

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

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

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

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

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

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

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

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

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

在可持久化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('
');
        }
    }
    return 0;
}
可持久化Trie树

提高:可持久化 SegmentTree

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

这个题目其实就是可持久化思想的运用,我这里写了一个维护区间的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('
');
        }
    }
    return 0;
}
例题A:可持久化线段树(区间max)

虽然上面的可持久化Segment确实可以解决这个问题但是,杀鸡焉用牛刀?

这里是一个裸的可持久化数组的写法(以后会衍生出类似于可持久化并查集等数据结构!)

# include <bits/stdc++.h>
using namespace std;
const int N=1e6+10;
struct rec{
    int lc,rc,val;
}tree[N*20];
int n,m,root[N],tot;
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 build(int &rt,int l,int r)
{
    rt=++tot;
    if (l==r) { tree[rt].val=read(); return;}
    int mid=(l+r)>>1;
    build(tree[rt].lc,l,mid);
    build(tree[rt].rc,mid+1,r);
}
int query(int rt,int l,int r,int pos)
{
    if (l==r) return tree[rt].val;
    int mid=(l+r)>>1;
    if (pos<=mid) return query(tree[rt].lc,l,mid,pos);
    else return query(tree[rt].rc,mid+1,r,pos);
}
void update(int last,int &rt,int l,int r,int pos,int opx)
{
    rt=++tot; tree[rt]=tree[last];
    if (l==r) { tree[rt].val=opx; return;}
    int mid=(l+r)>>1;
    if (pos<=mid) update(tree[last].lc,tree[rt].lc,l,mid,pos,opx);
    else update(tree[last].rc,tree[rt].rc,mid+1,r,pos,opx);
}
int main()
{
    n=read();m=read();
    build(root[0],1,n);
    for (int i=1;i<=m;i++) {
        int id=read(),op=read(),loc=read();
        if (op==1) {
            int val=read();
            update(root[id],root[i],1,n,loc,val);
        } else
        {
            int ans=query(root[id],1,n,loc);
            root[i]=root[id];
            printf("%d
",ans);
        }
    }
    return 0;
}
例题A:可持久化数组(基于可持久化线段树)

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

例题B:  P3834 【模板】可持久化线段树 1(主席树)

不考虑l和r的限制,用线段树维护[L,R]有多少数已经被插入了,

那么我们就可以用线段树对值域的二分代替两个二分答案了。

首先吧原数组离散化,并记录离散化以后数u对应原来的哪一个整数(C++ STL)

对于离散化以后的值域[1,T]建立一棵可持久化线段树(主席树),

对于每一个原来的数a[i],把它离散化后的值d[i]加入主席树,这样我们就可以用主席树维护出前i个数有多少数(离散化后)落在值域[L,R]上了。

这对于我们后面的处理是有帮助的!

考虑我们建树的格式是一样的,若考虑opl,opr的限制,在每一个节点上若是在区间opl,opr上的数的个数那么就是这个节点中以opl-1为根的线段树该节点的个数减去以opr为根的线段树该节点的个数,这个东西就是[opl,opr]中在值域[L,R](该节点值域)中塞入元素的个数。

大概就做完了,先给出query函数

//p是后面那个节点(从root[opr]开始),q是前面那个节点(从root[opl-1])开始
//当前点的值域是[l,r],求区间k小数
int query(int p,int q,int l,int r,int k)
{
    if (l==r) return l;
    int mid=(l+r)>>1;
    int ret=tree[tree[p].lc].val-tree[tree[q].lc].val;
    if (k<=ret) return query(tree[p].lc,tree[q].lc,l,mid,k);
    else         return query(tree[p].rc,tree[q].rc,mid+1,r,k-ret);
}

整个代码:Code:

要注意p,q哪个是前面的!!!

// luogu-judger-enable-o2
# include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
map<int,int>H;
int n,m,tot;
int a[N],b[N],root[N];
struct Segment_Tree{
    int lc,rc,val;
}tree[N*20];
vector<int>tmp;
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;
}
inline void write(int x)
{
    if (x<0) { x=-x; putchar('-');}
    if (x>9) write(x/10);
    putchar('0'+x%10);
}
void lisanhua(int *d,int len)
{
    tmp.clear();
    for (int i=1;i<=len;i++) tmp.push_back(d[i]);
    sort(tmp.begin(),tmp.end());
    vector<int>::iterator it=unique(tmp.begin(),tmp.end());
    for (int i=1;i<=len;i++)
     d[i]=lower_bound(tmp.begin(),it,d[i])-tmp.begin()+1;
}
int build(int l,int r)
{
    int p=++tot;
    if (l==r) { tree[p].val=0; return p;}
    int mid=(l+r)>>1;
    tree[p].lc=build(l,mid);
    tree[p].rc=build(mid+1,r);
    tree[p].val=tree[tree[p].lc].val+tree[tree[p].rc].val;
    return p;
}
int insert(int now,int l,int r,int x)
{
    int p=++tot;
    tree[p]=tree[now];
    if (l==r) { tree[p].val++; return p;}
    int mid=(l+r)>>1;
    if (x<=mid) tree[p].lc=insert(tree[now].lc,l,mid,x);
    else         tree[p].rc=insert(tree[now].rc,mid+1,r,x);
    tree[p].val=tree[tree[p].lc].val+tree[tree[p].rc].val;
    return p;
}
int query(int p,int q,int l,int r,int k)
{
    if (l==r) return l;
    int mid=(l+r)>>1;
    int ret=tree[tree[p].lc].val-tree[tree[q].lc].val;
    if (k<=ret) return query(tree[p].lc,tree[q].lc,l,mid,k);
    else         return query(tree[p].rc,tree[q].rc,mid+1,r,k-ret);
}
int main()
{
    n=read();m=read();
    int T=0,l,r,k;
    for (int i=1;i<=n;i++) b[i]=a[i]=read();
    lisanhua(b,n);
    for (int i=1;i<=n;i++) H[b[i]]=a[i],T=max(T,b[i]);
    for (int i=1;i<=n;i++) root[i]=insert(root[i-1],1,T,b[i]);
    for (int i=1;i<=m;i++) {
        l=read();r=read();k=read();
        int ans=query(root[r],root[l-1],1,T,k);
        write(H[ans]); putchar('
');
    }
    return 0;
}
线上值域主席树.cpp

例题C: P2633 Count on a tree 【建议到bzoj2588评测

先离散化点权,然后在每个点在值域[1,T]上建立可持久化线段树。

那么一个节点的线段树表示,从根1到该节点这条路径上在值域上塞入了多少个数!

树上主席树,对于每一个点从他的父亲基础上增加当前的点的权。

然后考虑静态在线询问,一条路径u到v,第k大权,

那么由树上差分知,(u,v)的信息=(1,u)信息+(1,v)信息-(1,lca)信息-(1,lca的父亲)信息

画个图你就明白了:

代码:Code:

# include <cstdio>
# include <vector>
# include <algorithm>
# include <map>
using namespace std;
const int N=1e5+10;
struct Segment_Tree{ int ls,rs,cnt; }t[N*20];
struct rec{ int pre,to; }a[N<<1];
int root[N*20],g[N][30],head[N],dep[N],val[N],b[N];
int n,m,tot,Edge,T;
map<int,int>H;
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;
}
inline void write(int x)
{
    if (x<0) x=-x,putchar('-');
    if (x>9) write(x/10);
    putchar('0'+x%10);
}
void adde(int u,int v)
{
    a[++Edge].pre=head[u];
    a[Edge].to=v;
    head[u]=Edge;
}
vector<int>tmp;
void lsh(int *d,int len)
{
    tmp.clear();
    for (int i=1;i<=len;i++) tmp.push_back(d[i]);
    sort(tmp.begin(),tmp.end());
    vector<int>::iterator it=unique(tmp.begin(),tmp.end());
    for (int i=1;i<=len;i++)
     d[i]=lower_bound(tmp.begin(),it,d[i])-tmp.begin()+1;
}
#define lson t[rt].ls,l,Mid
#define rson t[rt].rs,Mid+1,r
#define Mid ((l+r)>>1)
void insert(int last,int &rt,int l,int r,int val)
{
    rt=++tot; t[rt]=t[last];
    if (l==r) {t[rt].cnt++;return;}
    if (val<=Mid) insert(t[last].ls,lson,val);
    else insert(t[last].rs,rson,val);
    t[rt].cnt=t[t[rt].ls].cnt+t[t[rt].rs].cnt;
}
int query(int ru,int rv,int rlca,int rfa,int l,int r,int k)
{
    if (l==r) return l;
    int ret=t[t[ru].ls].cnt+t[t[rv].ls].cnt-t[t[rlca].ls].cnt-t[t[rfa].ls].cnt;
    if (k<=ret) return query(t[ru].ls,t[rv].ls,t[rlca].ls,t[rfa].ls,l,Mid,k);
    else         return query(t[ru].rs,t[rv].rs,t[rlca].rs,t[rfa].rs,Mid+1,r,k-ret);
}
#undef lson
#undef rson
#undef Mid
void dfs(int u,int fath)
{
    g[u][0]=fath; dep[u]=dep[fath]+1;
    insert(root[fath],root[u],1,T,val[u]);
    for (int i=head[u];i;i=a[i].pre) {
        int v=a[i].to; if (v==fath) continue;
        dfs(v,u);
    }
}
int LCA(int u,int v)
{
    if (dep[u]<dep[v]) swap(u,v);
    for (int i=21;i>=0;i--)
     if (dep[g[u][i]]>=dep[v]) u=g[u][i];
    if (u==v) return u;
    for (int i=21;i>=0;i--)
     if (g[u][i]!=g[v][i]) u=g[u][i],v=g[v][i];
    return g[u][0];
}
int main()
{
    //freopen("1.out","w",stdout);
    n=read();m=read(); T=0;
    for (int i=1;i<=n;i++) b[i]=val[i]=read();
    lsh(b,n);
    for (int i=1;i<=n;i++) H[b[i]]=val[i],T=max(val[i]=b[i],T);
    int u,v,r,ans=0;
    for (int i=1;i<n;i++) {
        u=read();  v=read();
        adde(u,v); adde(v,u);
    }
    dfs(1,0);
    for (int i=1;i<=21;i++)
     for (int j=1;j<=n;j++)
      g[j][i]=g[g[j][i-1]][i-1];
    while (m--) {
        u=read()^ans;v=read();r=read();
        int lca=LCA(u,v);
        ans=query(root[u],root[v],root[lca],root[g[lca][0]],1,T,r);
        ans=H[ans];
        write(ans); putchar('
');
    }
    return 0;
}
例题C:count on a tree

例题D:P4602 [CTSC2018]混合果汁

其实在算法竞赛(高端的那种)可做的题目只有数据结构题了。。。

我们考虑新鲜出炉的CTSC2018中一道混合果汁,一个显然的二分的算法呼之欲出。

但是一看它m个询问,立马否掉了,因为复杂度是O(mn log2 n)

但是思想比较重要,我们二分一个美味度D,然后吧所有美味度大于等于D的按照价格排序单减排序,

依次往后取到第k个,使前面的li之和大于等于L,又使前面的li*pi小于等于G,

这就意味这前面的这部分(1到k-1)是全取li升的,对于m个询问这种算法是TLE的。

定睛一看p的范围这么小(1e5?)其实大点没事的离散化(只不过麻烦那么一点点...)

然后就可持久化线段树维护一下p,具体的方法如下:

先把读进来的果汁种类按照d递减排序(意味着前面可以取,也可以不取)

这是由于insert是在前面的基础上操作(你访问的时候只能访问前面的,后面不行)。所以只能d递减排序。

然后对于所有在线询问,二分一个答案D,check的时候,看花掉这么多前最多可以得到几升和读入的体积比较。

具体的作法就是在query的时候能扔的扔掉类二分下去。

Code:(特别注意最后可能会取完也可能由于代价的限制取不完)

# include <cstdio>
# include <algorithm>
# define int long long
using namespace std;
const int N=1e5+10;
struct Seqment_Tree{
    int sum,siz,ls,rs;
}t[N*20];
struct rec{
    int d,p,l;
}a[N];
int n,m,tot;
int root[N*20];
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;
}
inline void write(int x)
{
    if (x>9) write(x/10);
    putchar('0'+x%10);
}
bool cmp(rec a,rec b){return a.d>b.d;}
void insert(int pre,int &rt,int l,int r,int x,int v)
{
    rt=++tot; t[rt]=t[pre];
    t[rt].sum+=x*v;
    t[rt].siz+=v;
    if (l==r) return;
    int mid=(l+r)/2;
    if (x<=mid) insert(t[pre].ls,t[rt].ls,l,mid,x,v);
    else insert(t[pre].rs,t[rt].rs,mid+1,r,x,v);
}
int query(int rt,int l,int r,int x)
{
    if (!rt) return 0;
    if (l==r) return min(x/l,t[rt].siz);
    int mid=(l+r)/2;
    if (t[t[rt].ls].sum>x) return query(t[rt].ls,l,mid,x);
    else return query(t[rt].rs,mid+1,r,x-t[t[rt].ls].sum)+t[t[rt].ls].siz;
}
signed main()
{
    n=read();m=read();
    int T=0;
    for (int i=1;i<=n;i++)
    a[i].d=read(),T=max(T,a[i].p=read()),a[i].l=read();
    sort(a+1,a+1+n,cmp);
    for (int i=1;i<=n;i++)
        insert(root[i-1],root[i],1,T,a[i].p,a[i].l);
     int g,w;
    while (m--) {
       g=read();w=read();
        int l=1,r=n,ans=0;
        while (l<=r) {
            int mid=(l+r)/2;
            if (query(root[mid],1,T,g)>=w) ans=mid,r=mid-1;
            else l=mid+1;
        }
        if (!ans) puts("-1");
        else write(a[ans].d),putchar('
');
    }
    return 0;
}
例题D:混合果汁代码

进阶:可持久化并查集

P3402 【模板】可持久化并查集

普通的并查集就不提了,可持久化并查集就是用个可持久化数组(线段树??)来维护这个并查集。

当然我们要考虑优化路径的方案(Hmm...就是保持这颗二叉树尽可能平均深度是log2n)

然而不能使用路径压缩。。那是因为一些非法出题人会卡爆long long,所以还是按秩合并比较稳妥。

即在合并两颗子树(根为u和v)把深度小的合到深度大的上面,并改变深度.

不妨一个个看下来这些函数。

首先两个函数分别是建可持久化数组、和可持久化数组pos位+1(方便后面按秩合并),这些是常规操作。

//建树
void build(int &rt,int l,int r)
{
    rt=++tot;
    if (l==r) { tree[rt].val=l; return;}
    int mid=(l+r)>>1;
    build(tree[rt].lc,l,mid);
    build(tree[rt].rc,mid+1,r);
}
//pos位+1
void update(int rt,int l,int r,int pos)
{
    if (l==r){ tree[rt].dep++; return ;}
    int mid=(l+r)>>1;
    if (pos<=mid) update(tree[rt].lc,l,mid,pos);
    else update(tree[rt].rc,mid+1,r,pos);
}

接下来一个就是求pos号元素是谁(访问,这个应该是基本操作),这个也比较简单:

//返回pos号元素的根
int query(int rt,int l,int r,int pos)
{
    if (l==r) return rt;
    int mid=(l+r)>>1;
    if (pos<=mid) return query(tree[rt].lc,l,mid,pos);
    else return query(tree[rt].rc,mid+1,r,pos);
}

然后接下来这个就是暴力找一个节点的祖先:

//找pos号元素对应的祖先
int find(int rt,int pos)
{
    int now=query(rt,1,n,pos);
    if (tree[now].val==pos) return now;
    return find(rt,tree[now].val);
}

再来一个merge函数表示把pos这个位值连接到to这个子树上:

这主要是利用可持久话操作(之前last和now并进)

最后把节点直接父亲指向to的直接父亲,深度置为一样:

void merge(int last,int &rt,int l,int r,int pos,int to)
{
    rt=++tot; tree[rt]=tree[last];
    if (l==r) {
        tree[rt].val=to;
        tree[rt].dep=tree[last].dep;
        return;
    }
    int mid=(l+r)>>1;
    if (pos<=mid) merge(tree[last].lc,tree[rt].lc,l,mid,pos,to);
    else merge(tree[last].rc,tree[rt].rc,mid+1,r,pos,to);
}

后面并查集的操作就和普通并查集一样就行!!!

void work_merge(int i,int x,int y)//在i-1的基础上合并x,y节点
{
    root[i]=root[i-1];
    int fx=find(root[i],x);
    int fy=find(root[i],y);
    if (tree[fx].val!=tree[fy].val) {
        if (tree[fx].dep>tree[fy].dep)  swap(fx,fy);
        merge(root[i-1],root[i],1,n,tree[fx].val,tree[fy].val);
     //深度小的fx连到深度大的fy去减小复杂度!!!
        if (tree[fx].dep==tree[fy].dep) update(root[i],1,n,tree[fy].val);
        //注意:是fx的深度小于fy的深度,所以要把fy的深度+1
    }
}
bool work_find(int i,int x,int y) //找第i个版本x和y是不是在一个集合中
{
    root[i]=root[i-1];
    int fx=find(root[i],x);
    int fy=find(root[i],y);
    return tree[fx].val==tree[fy].val;
}
void work_goback(int i,int id) //恢复到第id版本
{
    root[i]=root[id];
}

整个代码Code:

// luogu-judger-enable-o2
# include <bits/stdc++.h>
using namespace std;
const int N=2e5+10;
struct Tree{
    int val,lc,rc,dep;
}tree[N*20];
int n,m,tot;
int root[N*20];
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 build(int &rt,int l,int r)
{
    rt=++tot;
    if (l==r) { tree[rt].val=l; return;}
    int mid=(l+r)>>1;
    build(tree[rt].lc,l,mid);
    build(tree[rt].rc,mid+1,r);
}
void update(int rt,int l,int r,int pos)
{
    if (l==r){ tree[rt].dep++; return ;}
    int mid=(l+r)>>1;
    if (pos<=mid) update(tree[rt].lc,l,mid,pos);
    else update(tree[rt].rc,mid+1,r,pos);
}
void merge(int last,int &rt,int l,int r,int pos,int to)
{
    rt=++tot; tree[rt]=tree[last];
    if (l==r) {
        tree[rt].val=to;
        tree[rt].dep=tree[last].dep;
        return;
    }
    int mid=(l+r)>>1;
    if (pos<=mid) merge(tree[last].lc,tree[rt].lc,l,mid,pos,to);
    else merge(tree[last].rc,tree[rt].rc,mid+1,r,pos,to);
}
int query(int rt,int l,int r,int pos)
{
    if (l==r) return rt;
    int mid=(l+r)>>1;
    if (pos<=mid) return query(tree[rt].lc,l,mid,pos);
    else return query(tree[rt].rc,mid+1,r,pos);
}
int find(int rt,int pos)
{
    int now=query(rt,1,n,pos);
    if (tree[now].val==pos) return now;
    return find(rt,tree[now].val);
}
void work_merge(int i,int x,int y)
{
    root[i]=root[i-1];
    int fx=find(root[i],x);
    int fy=find(root[i],y);
    if (tree[fx].val!=tree[fy].val) {
        if (tree[fx].dep>tree[fy].dep)
         swap(fx,fy);
        merge(root[i-1],root[i],1,n,tree[fx].val,tree[fy].val);
        if (tree[fx].dep==tree[fy].dep) update(root[i],1,n,tree[fy].val);
    }
}
bool work_find(int i,int x,int y)
{
    root[i]=root[i-1];
    int fx=find(root[i],x);
    int fy=find(root[i],y);
    return tree[fx].val==tree[fy].val;
}
void work_goback(int i,int id)
{
    root[i]=root[id];
}
int main()
{
    n=read();m=read();
    build(root[0],1,n);
    for (int i=1;i<=m;i++) {
        int op=read();
        if (op==1) {
            int a=read(),b=read();
            work_merge(i,a,b);
        } else if (op==2) {
            int k=read();
            work_goback(i,k);
        } else if (op==3) {
            int a=read(),b=read();
            putchar(work_find(i,a,b)+'0');
            putchar('
');
        }
    }
    return 0;
}
可持久化并查集

给个NOI2018的例题: P4768 [NOI2018]归程

还有可持久化Treap、可持久化Splay。

....

自闭了。

(未完待续)

原文地址:https://www.cnblogs.com/ljc20020730/p/10353707.html