24 号要出去学习,趁现在先自己 xjb 学点啥。
基本概念 首先我们有一道模板题 。
我们所熟悉的树链剖分指的是重链剖分。也就是定义一个节点的子树大小最大的儿子为它的重儿子,连接重儿子的边叫做重边,若干条重边连在一起形成重链。除了重儿子以外的儿子被称为轻儿子,连接轻儿子的边叫做轻边。
这样一来,我们给原树的节点重新标号,使得一条重链上的所有节点的新标号是连续的,然后通过线段树或者树状数组等能够处理区间的数据结构进行维护,通过轻边合并两条重链上的信息。时间复杂度一般是 $O(n\log^{2}n)$。
但是这样有一个问题,因为线段树,又或者是树状数组,都是静态的数据结构,它们不能支持我们在上面 xjb 捣鼓。反映到原树上,这就意味着我们边的轻重只能也是静态的。这会带来很多不便。就比如说,我们现在要维护一片森林,要求支持动态连边 、 删边,同时需要查询某条链上的信息。
很明显这是重链剖分无法胜任的。为了解决这个问题,我们需要一种能够动态修改边的轻重的算法,也就是实链剖分。
实链剖分一般被称为 LCT,全称Link-Cat Tree Linear Challestend Transformation Link-Cut Tree,也就是动态树。
与重链剖分类似地,我们根据实际情况,随便钦定一个节点的某个儿子为它的实儿子,连接实儿子的边叫做实边,若干条实边连在一起形成实链。除了实儿子以外的儿子被称为虚儿子,连接虚儿子的边叫做虚边。
为了实现它,我们需要通过更加灵活的 Splay 来维护每一条实链。它具有如下性质:
每棵 Splay 维护的是一条原森林中深度严格递增的路径,也就是一条实链。
每个节点属于且仅属于一棵 Splay。
如果一个节点在原森林中有多个儿子,只有一个与它在同一颗 Splay 中,也就是实儿子。其他儿子所在的 Splay 的根节点有一根父指针指向这个节点,但是从这个节点访问不到它们。
不同于重链剖分,就算一个节点有至少一个儿子,也可以没有实儿子。
以下图片来自 https://wenku.baidu.com/view/75906f160b4e767f5acfcedb。
我们来看这样一棵树
其中粗线表示实边,虚线表示虚边。
它所对应的 Splay 森林可能长下面这样,每一个绿框内都是一棵 Splay。
当然这并不是唯一的。
为了方便,我先放出我 Splay 的代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 struct SplayNode { SplayNode *ftr,*ls,*rs; int val,sum,rev; inline int isRoot () { return ftr==NULL ||(ftr->ls!=this &&ftr->rs!=this ); } inline void reverse () { std ::swap(ls,rs); rev^=1 ; } inline void pushDown () { if (rev){ if (ls!=NULL ) ls->reverse(); if (rs!=NULL ) rs->reverse(); rev=0 ; } } inline void pushUp () { sum=val; if (ls!=NULL ) sum^=ls->sum; if (rs!=NULL ) sum^=rs->sum; } }; SplayNode mempool[maxn+1 ]; inline void rotate (re SplayNode* p) { re SplayNode* q=p->ftr; q->pushDown(); p->pushDown(); p->ftr=q->ftr; if (p->ftr!=NULL ){ if (p->ftr->ls==q) p->ftr->ls=p; if (p->ftr->rs==q) p->ftr->rs=p; } if (q->rs==p){ q->rs=p->ls; if (q->rs!=NULL ) q->rs->ftr=q; p->ls=q; q->ftr=p; } else { q->ls=p->rs; if (q->ls!=NULL ) q->ls->ftr=q; p->rs=q; q->ftr=p; } q->pushUp(); p->pushUp(); } inline void splay (re SplayNode* p) { for (;!p->isRoot();rotate(p)) if (!p->ftr->isRoot()) rotate((p->ftr->ftr->ls==p->ftr)==(p->ftr->ls==p)?p->ftr:p); }
access 首先我们有一个基本操作
首先有一个问题是,这个单词怎么读?
/ək'ses/
?不不不应该是 /'ækses/
。
如果您有兴趣,可以在这里 看看自己以前都读错了多少单词(
这个函数的作用是打通指定节点到根节点的路径,将这条路径修改成实链,并抛弃指定节点自身的实儿子。
我们来看看这个函数的具体过程。还是上面的例子,现在我们调用 access(N)
,整棵树会变成这样
虽然说好像图上用的还是轻重……不过这些细节就不要在意啦(
首先我们调用 splay(N)
,令 $\text{N}$ 成为它所在的 Splay 的根节点,然后它所在的实链中再往下的部分就到了它的右子树中,我们直接回收它的右儿子指针即可。需要注意的是,我们并没有切断这条边,只是让它变虚,因此它的右儿子的父指针不应该被修改。
向上找到 $\text{N}$ 的父亲 $\text{I}$,调用 splay(I)
,回收 $\text{I}$ 的右儿子指针。不过这一次,我们需要再令其指向 $\text{N}$,然后 $\text{N}$ 就成了 $\text{I}$ 的实儿子了。
然后继续向上,找到 $\text{I}$ 的父亲 $\text{H}$,调用 splay(H)
,并令 $\text{H}$ 的右儿子指针指向 $\text{I}$。
最后一步,找到 $\text{H}$ 的父亲 $\text{A}$,调用 splay(A)
,并令 $\text{A}$ 的右儿子指针指向 $\text{H}$。
然后我们看到,$\text{N}$ 和 $\text{A}$ 到了同一颗 Splay 中,完成任务,返回。
代码实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 inline void access (re SplayNode* p) { splay(p); p->pushDown(); p->rs=NULL ; p->pushUp(); for (re SplayNode* q=p;q->ftr!=NULL ;q=q->ftr){ splay(q->ftr); q->ftr->pushDown(); q->ftr->rs=q; q->ftr->pushUp(); } splay(p); }
findRoot 有了 access
,我们就可以随心所欲的瞎搞了。比如说
1 SplayNode* findRoot (SplayNode*)
返回给定节点所在的树的树根,也就是调用完 access
后它所在的实链上深度最小的节点。
因为 access
的最后自带了一个 splay
,这个时候给定节点已经是 Splay 的根节点了,我们直接循环跳左儿子指针即可。
1 2 3 4 5 6 inline SplayNode* findRoot (re SplayNode* p) { access(p); for (;p->ls!=NULL ;p=p->ls); splay(p); return p; }
makeRoot && split 现在我们要提取出树上两个给定节点之间的路径。但是我们知道,这样的路径不一定满足深度严格递增,也就是说,它可能不能够出现在一棵 Splay 中。
不过办法总是有的。比如说我们指定了两个节点 $x$ 和 $y$,我们先调用 access(x)
,然后考虑翻转 $x$ 的子树之后会发生什么。
access
结束后,$x$ 没有右子树,翻转之后就没有了左子树,也就是说,现在没有比 $x$ 的深度更小的节点了。换句话说就是,$x$ 现在成为了树根。
那么这样一来,$x$ 到 $y$ 的路径就一定满足深度严格递增了,我们只需调用一次 access(y)
就可以把它抽出来。
我们实现下面两个函数
1 void makeRoot (SplayNode*)
令给定节点成为树根。
1 void split (SplayNode*,SplayNode*)
抽出给定的两个节点之间的路径。不过虽然说模板题保证联通,还是有必要稍微考虑一下不连通的情况的。
1 2 3 4 5 6 7 8 9 inline void makeRoot (re SplayNode* p) { access(p); p->reverse(); } inline void split (re SplayNode* p,re SplayNode* q) { makeRoot(p); access(q); }
link 我们需要实现函数
1 void link (SplayNode*,SplayNode*)
在给定的两个节点间连一条边。特殊地,如果说给定的两个节点已经联通,什么都不做直接返回。不过这是模板题的要求,有些题可能会让你输出操作失败,这种情况下改一下返回值就行。
思路很简单。假设我们指定 $x$ 和 $y$ 两个节点,首先调用 makeRoot(x)
,然后检查 findRoot(y)
的返回值。如果不是 $x$,说明两个节点不连通,将 $x$ 的父指针指向 $y$;否则,即 findRoot(y)==x
,说明两个节点联通,直接返回。
1 2 3 4 5 inline void link (re SplayNode* p,re SplayNode* q) { makeRoot(p); if (findRoot(q)!=p) p->ftr=q; }
cut 我们需要实现函数
1 void cut (SplayNode*,SplayNode*)
切断给定的两个节点之间的边。不存在就什么也不做。
需要注意的是,两个节点 $x$ 和 $y$ 之间直接有边相连,不仅要求 $x$ 与 $y$ 联通,还要求它们在 Splay 中是相邻的两个节点。为了避免讨论深度的大小关系,我们先调用 makeRoot(x)
,此时 $y$ 应该是 $x$ 的右儿子,并且它不能有左儿子。
1 2 3 4 5 6 7 inline void cut (re SplayNode* p,re SplayNode* q) { makeRoot(p); if (findRoot(q)==p&&q->ftr==p&&q->ls==NULL ){ q->ftr=p->rs=NULL ; p->pushUp(); } }
把上面这么一些东西写好之后,再根据题目要求搞一搞,您就可以切掉模板题了。
完整板子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 #include <cstdio> #include <algorithm> #define re register #define maxn 300000 namespace cltstream{ #define size 1048576 char cltin[size+1 ],*ih=cltin,*it=cltin; inline char gc () { #ifdef ONLINE_JUDGE if (ih==it){ it=(ih=cltin)+fread(cltin,1 ,size,stdin ); if (ih==it) return EOF; } return *ih++; #else return getchar(); #endif } char cltout[size+1 ],*oh=cltout,*ot=cltout+size; inline void pc (char c) { if (oh==ot){ fwrite(cltout,1 ,size,stdout ); oh=cltout; } *oh++=c; } #define clop() fwrite(cltstream::cltout,1,cltstream::oh-cltstream::cltout,stdout),cltstream::oh=cltstream::cltout #undef size template <typename _tp> inline void read (_tp& x) { int sn=1 ; char c=gc(); for (;c!=45 &&(c<48 ||c>57 )&&c!=EOF;c=gc()); if (c==45 &&c!=EOF) sn=-1 ,c=gc(); for (x=0 ;c>=48 &&c<=57 &&c!=EOF;x=(x<<3 )+(x<<1 )+(c^48 ),c=gc()); x*=sn; } template <typename _tp> inline void write (_tp x,char text=-1 ) { if (x<0 ) pc(45 ),x=-x; if (!x) pc(48 ); else { int digit[22 ]; for (digit[0 ]=0 ;x;digit[++digit[0 ]]=x%10 ,x/=10 ); for (;digit[0 ];pc(digit[digit[0 ]--]^48 )); } if (text>=0 ) pc(text); } template <typename _tp> inline void swap (_tp& x,_tp& y) { _tp z=x; x=y; y=z; } } struct LinkCutTree { struct SplayNode { SplayNode *ftr,*ls,*rs; int val,sum,rev; inline int isRoot () { return ftr==NULL ||(ftr->ls!=this &&ftr->rs!=this ); } inline void reverse () { std ::swap(ls,rs); rev^=1 ; } inline void pushDown () { if (rev){ if (ls!=NULL ) ls->reverse(); if (rs!=NULL ) rs->reverse(); rev=0 ; } } inline void pushUp () { sum=val; if (ls!=NULL ) sum^=ls->sum; if (rs!=NULL ) sum^=rs->sum; } }; SplayNode mempool[maxn+1 ]; inline void rotate (re SplayNode* p) { re SplayNode* q=p->ftr; q->pushDown(); p->pushDown(); p->ftr=q->ftr; if (p->ftr!=NULL ){ if (p->ftr->ls==q) p->ftr->ls=p; if (p->ftr->rs==q) p->ftr->rs=p; } if (q->rs==p){ q->rs=p->ls; if (q->rs!=NULL ) q->rs->ftr=q; p->ls=q; q->ftr=p; } else { q->ls=p->rs; if (q->ls!=NULL ) q->ls->ftr=q; p->rs=q; q->ftr=p; } q->pushUp(); p->pushUp(); } inline void splay (re SplayNode* p) { for (;!p->isRoot();rotate(p)) if (!p->ftr->isRoot()) rotate((p->ftr->ftr->ls==p->ftr)==(p->ftr->ls==p)?p->ftr:p); } inline void build (re int n) { for (re int i=1 ;i<=n;++i){ re SplayNode* p=mempool+i; p->ftr=p->ls=p->rs=NULL ; cltstream::read(p->val); p->sum=p->val; p->rev=0 ; } } inline void access (re SplayNode* p) { splay(p); p->pushDown(); p->rs=NULL ; p->pushUp(); for (re SplayNode* q=p;q->ftr!=NULL ;q=q->ftr){ splay(q->ftr); q->ftr->pushDown(); q->ftr->rs=q; q->ftr->pushUp(); } splay(p); } inline SplayNode* findRoot (re SplayNode* p) { access(p); for (;p->ls!=NULL ;p=p->ls); splay(p); return p; } inline void makeRoot (re SplayNode* p) { access(p); p->reverse(); } inline void split (re SplayNode* p,re SplayNode* q) { makeRoot(p); access(q); } inline void link (re SplayNode* p,re SplayNode* q) { makeRoot(p); if (findRoot(q)!=p) p->ftr=q; } inline void cut (re SplayNode* p,re SplayNode* q) { makeRoot(p); if (findRoot(q)==p&&q->ftr==p&&q->ls==NULL ){ q->ftr=p->rs=NULL ; p->pushUp(); } } inline int queryPathXorSum (re int x,re int y) { split(mempool+x,mempool+y); return (mempool+y)->sum; } inline void createEdge (re int x,re int y) { link(mempool+x,mempool+y); } inline void destoryEdge (re int x,re int y) { cut(mempool+x,mempool+y); } inline void modifyVertice (re int x,re int y) { splay(mempool+x); (mempool+x)->val=y; (mempool+x)->pushUp(); } void printTree (re SplayNode* p) { if (p!=NULL ){ p->pushDown(); printTree(p->ls); printf ( "%d(%d,%d,%d)\n" , p-mempool, p->ftr!=NULL ?p->ftr-mempool:-1 , p->ls!=NULL ?p->ls-mempool:-1 , p->rs!=NULL ?p->rs-mempool:-1 ); printTree(p->rs); } } }; int n,m;LinkCutTree QAQ; int main () {\ cltstream::read(n); cltstream::read(m); QAQ.build(n); for (re int i=1 ;i<=m;++i){ int opt,x,y; cltstream::read(opt); cltstream::read(x); cltstream::read(y); switch (opt){ case 0 : cltstream::write(QAQ.queryPathXorSum(x,y),10 ); break ; case 1 : QAQ.createEdge(x,y); break ; case 2 : QAQ.destoryEdge(x,y); break ; case 3 : QAQ.modifyVertice(x,y); break ; } } clop(); return 0 ; }
是不是感觉很好写呢(
用 LCT 维护子树信息 咕咕咕。