• 动态树 Link-Cut Trees


    动态树

    动态树问题, 即要求我们维护一个由若干棵子结点无序的有根树组成的森林。

    要求这个数据结构支持对树的分割、合并,对某个点到它的根的路径的某些操作,以及对某个点的子树进行的某些操作。

    在这里我们考虑一个简化的动态树问题,它只包含对树的形态的操作和对某个点到根的路径的操作:

    维护一个数据结构,支持以下操作:

    • MAKE TREE() — 新建一棵只有一个结点的树。

    • CUT(v) — 删除 v 与它的父亲结点 parent(v) 的边,相当于将点 v 的子树分离了出来。

    • JOIN(v,w) — 让 v 成为 w 的新的儿子。其中 v 是一棵树的根结点,并且 v 和 w 是不同的两棵树中的结点。

    • FIND ROOT(v) — 返回 v 所在的树的根结点。搞清了这个问题,我们也容易扩充这个数据结构,维护每个点到它所属的树的根结点的路径的一些信息,例如权和、边权的最大值、路径长度等。

    Link-Cut Trees

    Link-Cut Trees 是由 Sleator 和 Tarjan 发明的解决这类动态树问题的一种数据结构。

    这个数据结构可以在均摊 O(logn) 的时间内实现上述动态树问题的每个操作。

    如果没有用对树的形态的改变的话,我们可以用树链剖分,把树剖分成许多条链并维护上面的权值信息。

    然而对于树的形态的改变,树链的剖分方案也要改变,我们借助 Splay 的思想来动态维护许多树链(称为 Link-Cut Trees)。

    Link-Cut Trees 的定义

    称一个点被访问过,如果刚刚执行了对这个点的 ACCESS 操作。如果结点 v 的子树中,最后被访问的结点在子树 w 中,这里 w 是 v 的儿子, 那么就称 w 是 v 的 Preferred Child。如果最后被访问过的结点就是 v 本身,那么它没有 Preferred Child。每个点到它的 Preferred Child 的边称作 Preferred Edge。由 Preferred Edge 连接成的不可再延伸的路径称为 Preferred Path。这样,整棵树就被划分成了若干条 Preferred Path。对每条 Preferred Path,用这条路上的点的深度作为关键字,用一棵平衡树来维护它(在这棵平衡树中,每个点的左子树中的点,都在 Preferred Path 中这个点的上方;右子树中的点,都在 Preferred Path 中这个点的下方)。需要注意的是,这种平衡树必须支持分离与 合并。这里,我们选择 Splay Tree 作为这个平衡树的数据结构。我们把这棵平衡树称为一棵 Auxiliary Tree。知道了树 T 分解成的这若干条 Preferred Path,我们只需要再知道这些路径之间的连接关系,就可以表示出这棵树 T。用 Path Parent 来记录每棵 Auxiliary Tree 对应的 Preferred Path 中的最高点的父亲结点,如果这个 Preferred Path 的最高点就是根结点,那么令这棵 Auxiliary Tree 的 Path Parent 为 null。Link-Cut Trees 就是将要维护的森林中的每棵树 T 表示为若干个 Auxiliary Tree。并通过 Path Parent 将这些 Auxiliary Tree 连接起来的数据结构。

    实际上,对于定义要从两方面去理解:

    首先是逻辑结构,存在着一棵或多棵树,这是在逻辑上存在的,我们要做的就是对这个森林进行操作与维护;

    其次是物理结构,我们并没有直接储存这些树,而是把树剖分成了多条链,每条链作为一棵 Splay。Splay 中的最小结点就是链的头结点。

    回顾一下定义:

    ACCESS(访问):又叫 Expose,对结点的访问操作。

    Preferred Child(最佳孩子):最近一次访问过的儿子。最近一次被访问过的结点没有 Preferred Child。

    Preferred Edge(最佳边):每个结点到 Preferred Child 的边。

    Preferred Path(最佳路径):连续的 Preferred Edge 组成的边。

    Auxiliary Tree(辅助树):在 Splay 上维护一条链,对于 Splay 上的每个结点,它的左子树上的点都在链的上方,右子树的点都在链的下方。

    Path Parent(路径的父亲):记录链上最高结点的父亲,也就是 Splay 最左边结点的父亲,用于表示链与链之间的关系。

    Link-Cut Trees 的操作

    Access

    ACCESS 操作是 Link-Cut Trees 的所有操作的基础。假设调用了过程 ACCESS(v),那么从点 v 到根结点的路径就成为一条新的 Preferred Path。如果路径上经过的某个结点 u 并不是它的父亲 parent(u) 的 Preferred Child,那么由于 parent(u) 的 Preferred Child 会变为 u,原本包含 parent(u) 的 Preferred Path 将不再包含结点 parent(u) 及其之上的部分。

    也就是说,过程 ACCESS 会改变逻辑上的树上的链剖分,它将结点 v 到根结点的路径作为一条新的链。而这条链会把旧的链切割开。

    在物理上的 Splay 的表现就是,Splay 中的一些树被断开与重组。

    下图为 Link-Cut Trees 的一个结构示意图,及一次 ACCESS 操作的前后对比图。

     

    首先,由于访问了点 v,那么它的 Preferred Child 应当消失。先将点 v 旋转到它所属的 Auxiliary Tree 的根,如果 v 在 v 所属的 Auxiliary Tree 中有右儿子(也就是 v 原来的 Preferred Child), 那么应该将 v 在 v 所属的 Auxiliary Tree 中的右子树(对应着它的原来的 Preferred Child 之下的 Preferred Path)从 v 所属的 Auxiliary Tree 中分离,并设置这个新的 Auxiliary Tree 的 Path Parent 为 v。然后,如果点 v 所属的 Preferred Path 并不包含根结点,设它的 Path Parent 为 u,那么需要将 u 旋转 到 u 所属的 Auxiliary Tree 的根,并用点 v 所属的 Auxiliary Tree 替换到点 u 所属的 Auxiliary Tree 中 点 u 的右子树,再将原来点 u 所属的 Auxiliary Tree 中点 u 的右子树的 Path Parent 设置为 u。如此操作,直到到达包含根结点的 Preferred Path。

    这里说的是对 Splay 的具体操作,对于访问的结点 v,伸展它到根,这样左子树的点在链的上方,右子树的点在链的下方。断开右子树即断开 v 在原树上与下方链的连接。

    之后在 Splay 上将链的父结点 u 伸展到根,用 v 替换 u 的右子树,表现在逻辑树上就是将 u 下方的旧链替换成我们新的链。

    在具体实现的时候,Splay 上的父亲与 Path Parent 可以在同一个数组上存储,当一个结点是它父结点的某个儿子时,它的父结点是 Splay 上的父结点,否则是 Path Parent。

    Find Root

    在 ACCESS(v) 之后,根结点一定是 v 所属的 Auxiliary Tree 的最小结点,我们先把 v 旋转到它所属 的 Auxiliary Tree 的根。再从 v 开始, 沿着 Auxiliary Tree 向左走,直到不能再向左,这个点就是我们要找的根结点。由于使用的是 Splay Tree 数据结构保存 Auxiliary Tree,我们还需要对根结点进行 Splay 操作。

    在访问过 v 之后,v 与树的根就在同一个链上了,在 Splay 中就是属于同一棵平衡树。这样 v 所在的平衡树的最左结点就是链的头结点即根结点。

    对根结点伸展是为了保证 Splay 的平衡。其实不做也没关系?

    Cut

    先访问 v,然后把 v 旋转到它所属的 Auxiliary Tree 的根,然后再断开 v 在它的所属 Auxiliary Tree 中 与它的左子树的连接,并设置。

    显然,v 到根的路径属于同一条链,保存在 Splay 上的同一棵平衡树中,此时 v 为平衡树的根,左子树在链的上方,右子树在链的下方,断开左子树就是断开逻辑树上的父边。

    Join

    先访问 v,然后修改 v 所属的 Auxiliary Tree 的 Path Parent 为 w,然后再次访问 v。

    实际上似乎有一种方法可以直接合并两棵无根树?具体写法见模板。

    大致思路是,对于要连接的两点 v、w,先访问 v,再访问 w,之后伸展 v 到根,此时 v 只有左子树没有右子树,因为 v 是剖分出的链的最底端,然后给 v 打一个延迟翻转标记,设他的父亲为 w。之后在每次伸展操作之前,找到要伸展的点的所有的父结点,然后从上到下维护翻转操作。

    这样做的原理是,访问 v 剖分出 v 到根的路径,然后将这个路径翻转,也就是树的其它结构不变,而这个把 v 提升到路径的最顶端,而原来的根成为了路径的最底短。

    算法模板

    SPOJ OTOCI

    3种操作,将不属于同一棵树的两点间建一条边,查询两点路径上的点权和,修改某点的权值。

      1 #include <iostream>
      2 #include <cstdio>
      3 #include <cstring>
      4 #include <algorithm>
      5 
      6 using namespace std;
      7 
      8 const int MaxNode=31000;
      9 
     10 int Lch[MaxNode];
     11 int Rch[MaxNode];
     12 int Pnt[MaxNode];
     13 int Data[MaxNode];
     14 int Sum[MaxNode];
     15 int Rev[MaxNode];
     16 int List[MaxNode];
     17 int Total;
     18 
     19 inline bool isRoot(int t){
     20     return (!Pnt[t]||(Lch[Pnt[t]]!=t&&Rch[Pnt[t]]!=t));
     21 }
     22 inline void Update(int cur){
     23     Sum[cur]=Sum[Lch[cur]]+Sum[Rch[cur]]+Data[cur];
     24 }
     25 void Reverse(int cur){
     26     if (!Rev[cur]) return;
     27     swap(Lch[cur],Rch[cur]);
     28     Rev[Lch[cur]]^=1;
     29     Rev[Rch[cur]]^=1;
     30     Rev[cur]=0;
     31 }
     32 void LeftRotate(int cur){
     33     if (isRoot(cur)) return;
     34     int pnt=Pnt[cur],anc=Pnt[pnt];
     35     Lch[pnt]=Rch[cur];
     36     if (Rch[cur]) Pnt[Rch[cur]]=pnt;
     37     Rch[cur]=pnt;
     38     Pnt[pnt]=cur;
     39     Pnt[cur]=anc;
     40     if (anc){
     41         if (Lch[anc]==pnt) Lch[anc]=cur;
     42         else if (Rch[anc]==pnt) Rch[anc]=cur;
     43     }
     44     Update(pnt);
     45     Update(cur);
     46 }
     47 void RightRotate(int cur){
     48     if (isRoot(cur)) return;
     49     int pnt=Pnt[cur],anc=Pnt[pnt];
     50     Rch[pnt]=Lch[cur];
     51     if (Lch[cur]) Pnt[Lch[cur]]=pnt;
     52     Lch[cur]=pnt;
     53     Pnt[pnt]=cur;
     54     Pnt[cur]=anc;
     55     if (anc){
     56         if (Rch[anc]==pnt) Rch[anc]=cur;
     57         else if (Lch[anc]==pnt) Lch[anc]=cur;
     58     }
     59     Update(pnt);
     60     Update(cur);
     61 }
     62 void Splay(int cur){
     63     int pnt,anc;
     64     List[++Total]=cur;
     65     for (int i=cur;!isRoot(i);i=Pnt[i]) List[++Total]=Pnt[i];
     66     for (;Total;--Total)
     67         if (Rev[List[Total]]) Reverse(List[Total]);
     68     while (!isRoot(cur)){
     69         pnt=Pnt[cur];
     70         if (isRoot(pnt)){// 父亲是根结点,做一次旋转
     71             if (Lch[pnt]==cur) LeftRotate(cur);
     72             else RightRotate(cur);
     73         }
     74         else{
     75             anc=Pnt[pnt];
     76             if (Lch[anc]==pnt){
     77                 if (Lch[pnt]==cur) LeftRotate(pnt),LeftRotate(cur);// 一条线
     78                 else RightRotate(cur),LeftRotate(cur);// 相反两次
     79             }
     80             else{
     81                 if (Rch[pnt]==cur) RightRotate(pnt),RightRotate(cur);// 一条线
     82                 else LeftRotate(cur),RightRotate(cur);// 相反两次
     83             }
     84         }
     85     }
     86 }
     87 int Expose(int u){
     88     int v=0;
     89     for (;u;u=Pnt[u]) Splay(u),Rch[u]=v,v=u,Update(u);
     90     for (;Lch[v];v=Lch[v]);
     91     return v;
     92 }
     93 void Modify(int x,int d){
     94     Splay(x);
     95     Data[x]=d;
     96     Update(x);
     97 }
     98 int Query(int x,int y){
     99     int rx=Expose(x),ry=Expose(y);
    100     if (rx==ry){
    101         for (int u=x,v=0;u;u=Pnt[u]){
    102             Splay(u);
    103             if (!Pnt[u]) return Sum[Rch[u]]+Data[u]+Sum[v];
    104             Rch[u]=v;
    105             Update(u);
    106             v=u;
    107         }
    108     }
    109     return -1;
    110 }
    111 bool Join(int x,int y){
    112     int rx=Expose(x),ry=Expose(y);
    113     if (rx==ry) return false;
    114     else{
    115         Splay(x);
    116         Rch[x]=0;
    117         Rev[x]=1;
    118         Pnt[x]=y;
    119         Update(x);
    120         return true;
    121     }
    122 }
    123 void Cut(int x){
    124     if (Pnt[x]){
    125         Expose(x);
    126         Pnt[Lch[x]]=0;
    127         Lch[x]=0;
    128         Update(x);
    129     }
    130 }
    131 int n,Q;
    132 
    133 void init(){
    134     Total=0;
    135     memset(Rev,0,sizeof(Rev));
    136     memset(Pnt,0,sizeof(Pnt));
    137     memset(Lch,0,sizeof(Lch));
    138     memset(Rch,0,sizeof(Rch));
    139     memset(Sum,0,sizeof(Sum));
    140 }
    141 char cmd[22];
    142 int main()
    143 {
    144     init();
    145     scanf("%d",&n);
    146     for (int i=1;i<=n;i++) scanf("%d",&Data[i]);
    147     scanf("%d",&Q);
    148     while (Q--){
    149         int x,y;
    150         scanf("%s%d%d",cmd,&x,&y);
    151         if (cmd[0]=='p'){
    152             Modify(x,y);
    153         }
    154         if (cmd[0]=='b'){
    155             if (Join(x,y)) printf("yes
    ");
    156             else printf("no
    ");
    157         }
    158         if (cmd[0]=='e'){
    159             int ans=Query(x,y);
    160             if (ans==-1) printf("impossible
    ");
    161             else printf("%d
    ",ans);
    162         }
    163     }
    164     return 0;
    165 }
    SPOJ OTOCI 

    SPOJ QTREE

    一棵树,两种操作,询问路径上的边权最大值,修改边权。

    由于LCT常数太大,我实在是搞不定这题。

    kuangbin巨巨的代码能卡着过去,在这里贴一下。

    http://www.cnblogs.com/kuangbin/p/3300217.html

      1 /* ***********************************************
      2 Author        :kuangbin
      3 Created Time  :2013-9-3 21:06:05
      4 File Name     :F:2013ACM练习专题学习动态树-LCTSPOJQTREE.cpp
      5 ************************************************ */
      6 
      7 #include <stdio.h>
      8 #include <string.h>
      9 #include <iostream>
     10 #include <algorithm>
     11 #include <vector>
     12 #include <queue>
     13 #include <set>
     14 #include <map>
     15 #include <string>
     16 #include <math.h>
     17 #include <stdlib.h>
     18 #include <time.h>
     19 using namespace std;
     20 
     21 //对一颗树,进行两个操作:
     22 //1.修改边权
     23 //2.查询u->v路径上边权的最大值
     24 const int MAXN = 10010;
     25 int ch[MAXN][2],pre[MAXN];
     26 int Max[MAXN],key[MAXN];
     27 bool rt[MAXN];
     28 void push_down(int r)
     29 {
     30     
     31 }
     32 void push_up(int r)
     33 {
     34     Max[r] = max(max(Max[ch[r][0]],Max[ch[r][1]]),key[r]);
     35 }
     36 void Rotate(int x)
     37 {
     38     int y = pre[x], kind = ch[y][1]==x;
     39     ch[y][kind] = ch[x][!kind];
     40     pre[ch[y][kind]] = y;
     41     pre[x] = pre[y];
     42     pre[y] = x;
     43     ch[x][!kind] = y;
     44     if(rt[y])
     45         rt[y] = false, rt[x] = true;
     46     else 
     47         ch[pre[x]][ch[pre[x]][1]==y] = x;
     48     push_up(y);
     49 }
     50 void P(int r)
     51 {
     52     if(!rt[r])P(pre[r]);
     53     push_down(r);
     54 }
     55 void Splay(int r)
     56 {
     57     //P(r);
     58     while( !rt[r] )
     59     {
     60         int f = pre[r], ff = pre[f];
     61         if(rt[f])
     62             Rotate(r);
     63         else if( (ch[ff][1]==f)==(ch[f][1]==r) )
     64             Rotate(f), Rotate(r);
     65         else
     66             Rotate(r), Rotate(r);
     67     }
     68     push_up(r);
     69 }
     70 int Access(int x)
     71 {
     72     int y = 0;
     73     do
     74     {
     75         Splay(x);
     76         rt[ch[x][1]] = true, rt[ch[x][1]=y] = false;
     77         push_up(x);
     78         x = pre[y=x];
     79     }
     80     while(x);
     81     return y;
     82 }
     83 //调用后u是原来u和v的lca,v和ch[u][1]分别存着lca的2个儿子
     84 //(原来u和v所在的2颗子树)
     85 void lca(int &u,int &v)
     86 {
     87     Access(v), v = 0;
     88     while(u)
     89     {
     90         Splay(u);
     91         if(!pre[u])return;
     92         rt[ch[u][1]] = true;
     93         rt[ch[u][1]=v] = false;
     94         push_up(u);
     95         u = pre[v = u];
     96     }
     97 }
     98 
     99 void change(int u,int k)
    100 {
    101     Access(u);
    102     key[u] = k;
    103     push_up(u);
    104 }
    105 void query(int u,int v)
    106 {
    107     lca(u,v);
    108     printf("%d
    ",max(Max[v],Max[ch[u][1]]));
    109 }
    110 
    111 struct Edge
    112 {
    113     int to,next;
    114     int val;
    115     int index;
    116 }edge[MAXN*2];
    117 int head[MAXN],tot;
    118 int id[MAXN];
    119 
    120 void addedge(int u,int v,int val,int index)
    121 {
    122     edge[tot].to = v;
    123     edge[tot].next = head[u];
    124     edge[tot].val = val;
    125     edge[tot].index = index;
    126     head[u] = tot++;
    127 }
    128 void dfs(int u)
    129 {
    130     for(int i = head[u];i != -1;i = edge[i].next)
    131     {
    132         int v = edge[i].to;
    133         if(pre[v] != 0)continue;
    134         pre[v] = u;
    135         id[edge[i].index] = v;
    136         key[v] = edge[i].val;
    137         dfs(v);
    138     }
    139 }
    140 void init()
    141 {
    142     tot = 0;
    143     memset(head,-1,sizeof(head));
    144 }
    145 int main()
    146 {
    147     //freopen("in.txt","r",stdin);
    148     //freopen("out.txt","w",stdout);
    149     int T;
    150     int n;
    151     int u,v,w;
    152     char op[20];
    153     scanf("%d",&T);
    154     while(T--)
    155     {
    156         init();
    157         scanf("%d",&n);
    158         for(int i = 0;i <= n;i++)
    159         {
    160             pre[i] = 0;
    161             ch[i][0] = ch[i][1] = 0;
    162             rt[i] = true;
    163         }
    164         Max[0] = -2000000000;
    165         for(int i = 1;i < n;i++)
    166         {
    167             scanf("%d%d%d",&u,&v,&w);
    168             addedge(u,v,w,i);
    169             addedge(v,u,w,i);
    170         }
    171         pre[1] = -1;
    172         dfs(1);
    173         pre[1] = 0;
    174         while(scanf("%s",&op) == 1)
    175         {
    176             if(op[0] == 'D')break;
    177             scanf("%d%d",&u,&v);
    178             if(op[0] == 'C')
    179                 change(id[u],v);
    180             else query(u,v);
    181         }
    182     }
    183     return 0;
    184 }
    kuangbin巨巨的动态树
  • 相关阅读:
    回调函数实例
    Java StringBuffer 和 StringBuilder 类
    excel被保护或者锁定时候按住alt和enter可以输入换行
    ArrayUtils.
    excel中在某一列上的所有单元格的前后增加
    decode(条件,值1,返回值1,值2,返回值2,...值n,返回值n,缺省值)
    正则表达式:Pattern类与Matcher类详解
    Cocos2d-x文本菜单
    msp430在ccsv5下出现的问题总结
    与TCP/IP协议的初次见面(一)
  • 原文地址:https://www.cnblogs.com/zinthos/p/3900225.html
Copyright © 2020-2023  润新知