ETT(Euler Tour Tree)学习笔记

dyf_DYF

2021-07-05 13:28:31

Personal

前言:笔者试图将 ETT 讲的通俗易懂,希望大部分读者能够学会。如果发现哪里讲错了也欢迎指出。 2021/7/6 update : 添加了 ETT 的 link-cut 操作,修改了例题。感谢@[cyffff](https://www.luogu.com.cn/user/365127)大佬提出的宝贵意见。 2021/7/8 update :整理文章结构,新增了目录。 2021/7/8 update :修了一个锅。感谢@[jerry3128](https://www.luogu.com.cn/user/27338)大佬指出错误。 2021/7/8 update :修了一个锅。感谢@[爱喝敌敌畏](https://www.luogu.com.cn/user/65602)大佬指出错误。 ------------ ## -1. 导入 当维护一个动态变化的树时,最常用的数据结构就是 Link-Cut Tree 了,但是有的毒瘤出题人总是出一些这样的阴间问题: + 把以 $x$ 连带它的子树整个接在节点 $y$ 的下面。 + 把以 $x$ 为根的子树权值都加上 $y$ 。 常规的LCT无法解决这样的问题,怎么办? 肯定有大佬会跳出来说:我会LCT维护虚子树信息! 然而这样简单的LCT就会细节巨多,稍微写错一点就会全部木大。我们需要一种更加适合我这种蒟蒻、更简单的数据结构—— **ETT** 。 **Notice :真正的ETT能够实现更加复杂的功能,然而写起来也更加困难,写题时经常使用的是使用牺牲了一部分功能的简化版,即伪ETT。本文主要介绍的也是伪 ETT 。** ## 0. 目录 1. 什么是 ETT ? 2. ETT 怎么写 ? 2.1. 前置芝士 : 括号序。 2.2. 前置芝士 : 伸展树。 2.3. 操作 1. 把以 $x$ 连带它的子树整个接在节点 $y$ 的下面。 2.4. 操作 2. 查询从 $x$ 到根的和。 2.5. 操作 3. 连接/断开一条从 $x$ 到 $y$ 的路径。 2.6. 操作 4. 给一个以 $x$ 为根的子树内所以节点权值集体加 $k$ 。 2.7. 关于时间复杂度。 3. 思考题。 4. 例题: P4219 。 5. 思考题答案。 6. 参考资料 ## 1. 什么是 ETT ? 在学习一种数据结构之前,我们要先弄懂这个数据结构要实现什么功能。 ETT 是维护动态树的数据结构,主要支持如下功能: - 把以 $x$ 连带它的子树整个接在节点 $y$ 的下面。 - 把以 $x$ 为根的子树权值都加上 $y$ 。 - 查询从 $x$ 到根的信息(如求和)。 - 连接一条从 $x$ 到 $y$ 的路径,保证原先 $x$ 和 $y$ 不连通。 - 断开一条从 $x$ 到 $y$ 的路径,保证原先 $x$ 和 $y$ 直接连通。 - …… ETT 比 LCT 支持更多操作,但是常数比 LCT 大得多。故如果不需要维护、修改子树信息,还是老老实实写 LCT 吧。 ## 2. ETT 怎么写? ### 2.1. 前置芝士:括号序(出栈入栈序) ~~话说欧拉环游树为什不用欧拉序反而用括号序。<-因为是伪 ETT 啊。~~ " DFS 进入某个节点的时候记录一个左括号 ( ,退出某个节点的啥时候记录一个右括号 ) 。每个节点会出现两次。相邻两个节点的深度相差 1。"(来自 OI-wiki ) 比如这样一棵树: ![graph](https://cdn.luogu.com.cn/upload/image_hosting/jh5d81kn.png) 它的括号序长这样(这里记下了节点编号,以入栈为正,出栈为负,而不是单单记录左右括号): `1 2 5 -5 6 -6 -2 3 7 -7 -3 4 -4 -1` 如果点上带权值呢? 简单转化一下,入栈时记录该节点的正权值,出栈时记录该节点的负权值。(下面将这种方式得到的括号序列简称权值括号序列)。 ### 2.2. 前置芝士:伸展树( splay ) 东西较多,很短的篇幅讲的也不能很细,就丢一篇过往的洛谷日报了 [洛谷日报62.Splay简易教程](https://tiger0132.blog.luogu.org/slay-notes) 学会了括号序和splay,就珂以正式学习ETT啦 ### 2.3. 操作 1. 把以 $x$ 连带它的子树整个接在节点 $y$ 的下面。 我们先来看看ETT最独特的操作。 还是上面那张图,我们把3号节点接到2号下面试试。 ![](https://cdn.luogu.com.cn/upload/image_hosting/89oxybx1.png) 好像没什么特别的,我们再写下它的括号序试试: 现在的序列是: `1 2 5 -5 6 -6 3 7 -7 -3 -2 4 -4 -1` 先前的序列是: `1 2 5 -5 6 -6 -2 3 7 -7 -3 4 -4 -1` 发现了什么?好像直接把 `3 7 -7 -3` 平移到了 `-2` 前面就行了? 别慌,再来试一下,这次我们把2号节点接到4号下面。 ![](https://cdn.luogu.com.cn/upload/image_hosting/g2g61t04.png) 再次写下它的括号序列: 现在的序列是: `1 4 2 5 -5 6 -6 3 7 -7 -3 -2 -4 -1` 先前的序列是: `1 2 5 -5 6 -6 3 7 -7 -3 -2 4 -4 -1` 这次把 `2 5 -5 6 -6 3 7 -7 -3 -2 ` 平移到了 `-4` 的前面。 看来真的只是平移就行了。 可以这样理解:一个节点 $x$ 的入点和出点之间维护了一整个以 $x$ 为根的子树信息,把这一段区间平移就相当于把整个以 $x$ 为根的子树平移。 **总结一下:把 $x$ 节点接到 $y$ 节点下面只需要一步:把从 $x$ 的入点到 $x$ 出点这一整段平移到 $y$ 出点前,或者 $y$ 的入点后。** ### 2.4. 操作2. 查询从 $x$ 到根的和。 只需要记录权值括号序列到这个节点的入点的前缀和就行了。~~读者可以自证不难~~ 以下面这张图为例: ![](https://cdn.luogu.com.cn/upload/image_hosting/ddha9z0w.png) 它的权值括号序列为:`5 6 2 -2 4 -4 -6 8 -8 -5` 查询从1到5的权值和,我们发现恰好是从第1个数到第5个数的和。 可以再感性理解一下,如果一个节点 $y$ 不在从根到查询的节点 $x$ 的路径上,要么在x的入点之前,与自己的出点相抵消,要么不在这段前缀和内。(如果还是觉得难以理解可以先深入地学习一下括号序)。 ~~好了,你已经学会了ETT了~~ 现在问题就转化为了怎么平移一段区间。 让我们隆重请出序列之神—— splay 。 ( splay 区间平移也有多种方法,这里只讲我自认为比较简单的一种。) 众所周知, splay 可以翻转区间(不会的可以看看[模板题](https://www.luogu.com.cn/problem/P3391)),我们来看看怎么把这个功能用起来。 ![](https://cdn.luogu.com.cn/upload/image_hosting/vd7gfzpc.png) 这是原序列,我们想把红色的部分移到蓝色的部分前面。 我们先把红蓝两部分一起翻转。 ![](https://cdn.luogu.com.cn/upload/image_hosting/dkcdwuxc.png) 红色部分就跑到蓝色前面去了。可惜是前后翻转的,我们把这两部分分别翻转过来。 ![](https://cdn.luogu.com.cn/upload/image_hosting/7jk8c8ti.png) 区间平移就完成啦。 ### 2.5. 操作 3. 连接/断开一条从 $x$ 到 $y$ 的路径。 **Notice : 有大佬指出这种方法只是换父亲,并不是真正的 link-cut ,因为本文主要介绍的是伪 ETT ,有想学习真正的 ETT 的读者可以自行学习。** 先看连边的情况。 这个好像有点复杂,因为,从树上平移跑到森林里去了。 我们再请出一张图: ![](https://cdn.luogu.com.cn/upload/image_hosting/4ebahjgb.png) 可以得到两个括号序: `1 3 2 -2 4 -4 -3 -1` `5 6 -6 -5` 现在我们把5号节点接在1号后面。 ![](https://cdn.luogu.com.cn/upload/image_hosting/57y5tzpk.png) 新的括号序只有一个括号序了: `1 5 6 -6 -5 3 2 -2 4 -4 -3 -1` 我们发现,这就是直接把第二个括号序列强塞在了1的后面。 反过来,当我们切断5号和1号之间的边时,就是直接把 `5 6 -6 -5` 单独分裂出来。 继续考虑用splay森林来维护这一堆括号序列 对于同一棵树上的修改,直接像刚才一样翻转就好。 当连接两个节点时,把深度较大的节点所在的 splay 合并到较浅的节点的 splay 中。 更具体地:使用上面的例子。 这是合并前的 splay : ![](https://cdn.luogu.com.cn/upload/image_hosting/cqsg5i4e.png) 我们把1提到根,再把第二个数(3)提到1的右子树。 ![](https://cdn.luogu.com.cn/upload/image_hosting/u6nfc9ty.png) 3的左子树就空出来啦,我们可以直接把这个 splay 接到这里,就完成了区间合并的操作。 ![](https://cdn.luogu.com.cn/upload/image_hosting/5uwh46k3.png) 更一般的,我们的操作分三步: 1. 找到插入位置前一个数,把它提到所在 splay 的根。 2. 找到插入位置后一个数,把它提到所在 splay 的根的右儿子。 3. 这时所在 splay 的根的右儿子的左儿子一定是空的,我们可以直接把要插入的 splay 接在这里,别忘了及时更新信息。 断开时同理,也分三步。 1. 找到断开区间的前驱,把它提到所在 splay 的根。 2. 找到断开区间的后继,把它提到所在 splay 的根的右儿子。 3. 这时所在 splay 的根的右儿子的左子树一定代表整个要断开的区间,我们可以直接把这里断开,当然也要及时更新信息。 Notice:这里讲到的方法是不支持换根的,如果要支持换根和动态连接和断开,可以进行一个ETT的看。 ### 2.6. 操作 4. 给一个以 $x$ 为根的子树内所以节点权值集体加 $k$ 。 简单,只要给 $x$ 的入点加 $k$ ,给 $x$ 的出点减 $k$ 。那么只有这个子树内的节点会受到影响加了 $k$ 。 ### 2.8. 关于时间复杂度。 因为都是 splay 维护的序列上的问题,单次修改和查询的时间复杂度当然也是 $O(logn)$ 啦。 ## 3. 思考题 你真的学会了 ETT ……吗?这里有几道思考题,答案在代码后面,读者可以思考思考,检测一下自己有没有真正理解 ETT 。 如果答对了,那么你真的学会了 ETT ,快去写道模板题练练手吧。 + ETT 可以实现 findroot 操作吗?如果可以,如何实现?如果不能,理由是什么? + 为什么断开区间的前驱和后继一定存在?(保证数据合法) + 如何计算以一个节点 $x$ 为根的子树大小? + 能不能用 ETT **维护路径和一样**维护路径最大值? 相关题目: - [ \[BZOJ3786\] 星系探索(模板题)](https://darkbzoj.tk/problem/3786) - [P4219 \[ BJOI2014 \] 大融合](https://www.luogu.com.cn/problem/P4219) ## 4. 例题 P4219 \[BJOI2014\]大融合 ### 题目大意: 给你 $n$ 个点,支持动态连边,查询一条边左右两边子树大小的乘积。保证数据合法。 ### 思路: 子树问题用 ETT 一般能简化许多,我们试试在这里使用 ETT 。 给的图是个森林,先给每个树指定一个根,把深度处理出来。 连边时按 ETT 的方法正常连即可。至于维护子树大小,写在思考题里了,没想出来可以看一下答案。 对于每个询问,我们求出深度较深的那个子树大小 $Siz$ ,用整颗树的大小减去 $Siz$ 就是这条边连接的另一棵子树的大小,相乘就是答案。 ### 代码: ```cpp //Don't be a K*******7 #include<cstdio> #include<cctype> #include<algorithm> #define fg(x) for(int i=g.headx[x],v=g.dat[i].t;i;i=g.dat[i].n,v=g.dat[i].t) #define ll long long const int maxn=200010; int in(){ int val=0,sig=1;char c=getchar(); while(!isdigit(c)){if(c=='-')sig=-1;c=getchar();} while(isdigit(c))val=((val<<3)+(val<<1)+(c^48)),c=getchar(); return val*sig; } char getaph(){ char c='~'; while(!isalpha(c))c=getchar(); return c; } namespace ETT{ #define l(x) dat[x].s[0] #define r(x) dat[x].s[1] #define f(x) dat[x].fa struct grap{ struct node{int n,t;}dat[maxn<<1]; int headx[maxn],tot; void add(int x,int y){ dat[++tot]=(node){headx[x],y}; headx[x]=tot; dat[++tot]=(node){headx[y],x}; headx[y]=tot; } }g; int d[maxn]; int S[maxn],T[maxn],rt[maxn],h=1,H; int X[maxn],Y[maxn],O[maxn]; struct DSU_desu{ int f[maxn]; int find(int x){return (f[x]==x)?x:f[x]=find(f[x]);} void init(int x){for(int i=1;i<=x;i++)f[i]=i;} void merge(int x,int y){ x=find(x),y=find(y); if(x==y)return ; if(d[x]>d[y])std::swap(x,y); f[y]=x; } }U; struct node{ int s[2],fa; int sz; }dat[maxn<<1]; int isrc(int x){return r(f(x))==x;} void pushup(int x){ dat[x].sz=1; if(l(x))dat[x].sz+=dat[l(x)].sz; if(r(x))dat[x].sz+=dat[r(x)].sz; } void rotate(int x,int &r){ int fa=f(x),gfa=f(fa),g=isrc(x); if(fa==r)r=x; else dat[gfa].s[isrc(fa)]=x; f(x)=gfa; f(dat[fa].s[g]=dat[x].s[!g])=fa; f(dat[x].s[!g]=fa)=x; pushup(fa);pushup(x); } void splay(int x,int &r){ while(x!=r){ if(f(x)!=r)rotate(isrc(x)^isrc(f(x))?x:f(x),r); rotate(x,r); } } int pre(int x){x=l(x);while(r(x))x=r(x);return x;} int nxt(int x){x=r(x);while(l(x))x=l(x);return x;} int split(int l,int r,int v){ splay(l,rt[v]);int L=pre(l); splay(r,rt[v]);int R=nxt(r); if(L==0||R==0)return r; splay(L,rt[v]); splay(R,r(L)); return l(R); } void dfs(int x,int fa){ d[x]=d[fa]+1; S[x]=++h;T[x]=++h;rt[x]=S[x]; r(S[x])=T[x];f(T[x])=S[x]; pushup(T[x]);pushup(S[x]); fg(x)if(v!=fa)dfs(v,x); } void init(int n){ for(int i=1;i<=n;i++)if(!d[i])dfs(i,0); U.init(n); } ll query(int x,int y){ if(d[x]>d[y])std::swap(x,y); int F=U.find(x); int pos=split(S[y],T[y],F); int count1=dat[pos].sz>>1; int countT=dat[rt[F]].sz>>1; int count2=countT-count1; return 1ll*count2*count1; } void merge(int x,int y){ if(d[x]>d[y])std::swap(x,y); int l=S[x]; int Fx=U.find(x),Fy=U.find(y); splay(l,rt[Fx]); int r=nxt(l); splay(r,r(l)); f(rt[Fy])=r;l(r)=rt[Fy]; U.merge(Fx,Fy); pushup(r);pushup(l); } void main(){ int n=in(),m=in(); for(int i=1;i<=m;i++){ char op=getaph(); int x=in(),y=in(); if(op=='A'){g.add(x,y);} O[i]=op;X[i]=x;Y[i]=y; } init(n); for(int i=1;i<=m;i++){ char op=O[i]; int x=X[i],y=Y[i]; if(op=='A')merge(x,y); else printf("%lld\n",query(x,y)); } } } int main(){ ETT::main(); return 0; } ``` 本人马蜂比较清奇且常数巨大,不喜轻喷。 另外这份代码只是通过了 P4219 ,不保证完全正确,如果您发现锅了欢迎来打我的脸。 ## 5. 思考题答案 1. 可以,找到整颗 splay 的最靠左的节点(括号序列的第一个值)。 2. 因为这段区间一定包含在它爸爸的入点和出点之间,不可能在整段序列的头部或尾部。 3. 类似删除:再分三步: 1. 找到 $x$ 的入点的前驱,把它提到所在 splay 的根。 2. 找到 $x$ 的出点的后继,把它提到所在 splay 的根的右儿子。 3. 同样这时所在 splay 的根的右儿子的左子树(为了方便叙述,记为子树 $Y$ )一定代表整个以 $x$ 为根的子树的括号序列,又因为这段括号序列同时包括这颗子树内所有点的出点和入点(每个点两次),我们直接将子树 $Y$ 的大小除以二作为答案。 4. 不行,因为最大值不具有区间可加性,不能保证求到的答案在这条路径上。(这并不代表 ETT 不能维护链最大值)。 ## 6. 参考资料 ~~本人版权意识薄弱~~ - [OI-wiki](https://oi-wiki.org/ds/ett/) - [图论在线作图工具](https://csacademy.com/app/graph_editor/) END