树专题(伸展树 / 树链剖分 / 动态树 学习笔记)
花了两天时间学了一下这三种数据结构。
总结 树上的修改与询问
伸展树:
支持Spaly的平衡树。Splay操作可以让节点x旋转到树的根节点。
伸展树在区间插入,区间删除,区间翻转,区间旋转的问题中应用较多。
具体而言,我们可以以区间的下标为关键字构造伸展树。
在x与x+1中插入一段区间,我们可以通过Spaly操作完成,将x旋转至根节点,将x+1旋转至根节点的右节点,此时,x+1的左节点为空,插入该左节点即可。
删除 [ l, r ] 区间通过Splay后,直接令根节点的右节点的左节点为空即可。
翻转操作,可以类似线段树加入懒惰标记即可。
旋转区间,复杂一点可以三次翻转。优秀的见Claris代码。
其中,Spaly操作的均摊复杂度为O(log(n)).(treap的复杂度为期望复杂度O(log(n)))
1 const int N = 1e5+200; 2 int a[N]; //输入数组 3 int val[N], mn[N], tag[N], size[N], son[N][2], f[N], tot, root; 4 bool rev[N]; 5 void rev1(int x) { //翻转以x为根的子树 6 if(!x) return ; 7 swap(son[x][0], son[x][1]); 8 rev[x] ^= 1; 9 } 10 void add1(int x,int p) { //以x为根的子树 +p 11 if(!x) return ; 12 val[x] += p; 13 mn[x] += p; 14 tag[x] += p; 15 } 16 void pb(int x){ 17 if(rev[x]){ 18 rev1(son[x][0]); 19 rev1(son[x][1]); 20 rev[x] = 0; 21 } 22 if(tag[x]){ 23 add1(son[x][0], tag[x]); 24 add1(son[x][1], tag[x]); 25 tag[x] = 0; 26 } 27 } 28 void up(int x){ 29 size[x] = 1, mn[x] = val[x]; 30 if(son[x][0]){ 31 size[x] += size[son[x][0]]; 32 if(mn[x] > mn[son[x][0]]) mn[x] = mn[son[x][0]]; 33 } 34 if(son[x][1]){ 35 size[x] += size[son[x][1]]; 36 if(mn[x] > mn[son[x][1]]) mn[x] = mn[son[x][1]]; 37 } 38 } 39 void rotate(int x){ 40 int y = f[x], w = son[y][1] == x; 41 son[y][w] = son[x][w^1]; 42 if(son[x][w^1]) f[son[x][w^1]] = y; 43 if(f[y]){ 44 int z = f[y]; 45 if(son[z][0] == y) son[z][0] = x; 46 if(son[z][1] == y) son[z][1] = x; 47 } 48 f[x] = f[y]; son[x][w^1] = y; f[y] = x; up(y); 49 } 50 void splay(int x, int w){ //splay操作, 使得x的父节点为w, 注意包含端节点的情况 51 //一般通过x = kth(x)找到第x小的节点, w = kth(w) 52 int s = 1, i = x, y; a[1] = x; 53 while(f[i]) a[++s] = i = f[i]; 54 while(s) pb(a[s--]); 55 while(f[x] != w){ 56 y = f[x]; 57 if(f[y] != w){ 58 if((son[f[y]][0]==y)^(son[y][0]==x)) rotate(x); 59 else rotate(y); 60 } 61 rotate(x); 62 } 63 if(!w) root = x; 64 up(x); 65 } 66 int build(int l, int r, int fa){ 67 int x = ++tot, mid = (l+r)>>1; 68 f[x] = fa; val[x] = a[mid]; 69 if(l < mid) son[x][0] = build(l, mid-1, x); 70 if(r > mid) son[x][1] = build(mid+1, r, x); 71 up(x); 72 return x; 73 } 74 int kth(int k){ 75 int x = root, tmp; 76 while(1){ 77 pb(x); 78 tmp = size[son[x][0]]+1; 79 if(k == tmp) return x; 80 if(k < tmp) x = son[x][0]; 81 else k -= tmp, x = son[x][1]; 82 } 83 }
树链剖分:
通过树的先序遍历(遍历子节点时,优先遍历子树最大的子节点),构造dfs序线段树。
每个节点都属于某条重链,重链的条数不超过 logn 条。
而每条重链在线段树上是一段连续区间。
由于任一轻儿子对应的子树大小要小于父节点所对应子树大小的一半,因此从一个轻儿子沿轻边向上走到父节点后 所对应的子树大小至少变为两倍以上,经过的轻边条数自然是不超过logn的。
然后由于重链都是间断的 (连续的可以合成一条),所以经过的重链的条数是不超过轻边条数+1的,因此经过重链的条数也是log级别的
故对于一条树上的路径,对应于线段树上的若干段区间,每次操作的复杂度为O(logn*logn).
树链剖分较多用于处理树上的路径问题。
如果我们的操作是权值加,询问是权值和,我们无需树链剖分操作,用树状数组或线段树即可。将所有操作化为点操作和子树操作即可。
复杂一点的需要进行变量分离操作。类似于用树状数组处理区间加区间查的问题
1 void dfs(int x){ 2 size[x]=1; 3 for(int i=g[x];i;i=nxt[i])if(v[i]!=f[x]){ 4 f[v[i]]=x,d[v[i]]=d[x]+1; 5 dfs(v[i]),size[x]+=size[v[i]]; 6 if(size[v[i]]>size[son[x]])son[x]=v[i]; 7 } 8 } 9 void dfs2(int x,int y){ 10 st[x]=++dfn;top[x]=y; 11 if(son[x])dfs2(son[x],y); 12 for(int i=g[x];i;i=nxt[i])if(v[i]!=son[x]&&v[i]!=f[x])dfs2(v[i],v[i]); 13 en[x]=dfn; 14 } 15 //查询x,y两点的lca 16 int lca(int x,int y){ 17 for(;top[x]!=top[y];x=f[top[x]])if(d[top[x]]<d[top[y]]) swap(x, y); 18 return d[x]<d[y]?x:y; 19 } 20 //x是y的祖先,查询x到y方向的第一个点 21 int lca2(int x,int y){ 22 int t; 23 while(top[x]!=top[y])t=top[y],y=f[top[y]]; 24 return x==y?t:son[x]; 25 } 26 //对x到y路径上的点进行操作 27 void chain(int x,int y){ 28 for(;top[x]!=top[y];x=f[top[x]]){ 29 if(d[top[x]]<d[top[y]]) swap(x, y); 30 change(st[top[x]],st[x]); 31 } 32 if(d[x]<d[y]) swap(x, y); 33 change(st[y],st[x]); 34 }
动态树:
动态树可以维护一个动态的森林,支持树的合并(两棵合并成一棵),分离(把某个点和它父亲点分开),动态LCA,查询修改某条链上的信息,换根。(lct仅限于链上的操作和形态的变化,处理子树操作需要top tree)
一棵树划分成若各条prefered path 每条prefered path用点的深度作为关键字用Splay平衡树(Auxiliary Tree)维护。
lct将每棵树表示为若干个Auxiliary Tree,并通过path parent将各个Auxiliary Tree连接起来。
关键操作access(x): 将root - x的路径作为一条prefered path,并且x不再有prefered child。
以bzoj2002弹飞绵羊为例,我们需要支持的操作为切边操作,连边操作,查询到根节点的距离。
我们维护辅助树的size大小即可。(同属于一条prefered path的所有节点构成一一棵辅助树。)
每次查询,通过access(x)操作(access操作后,x到根节点的所有节点构成一棵辅助树,x与其子节点不属于同一棵辅助树),再通过splay操作将x旋转至根节点,查询x节点对应的size大小即是x节点到根节点路径上的节点数。
access操作的均摊复杂度为O(log(n)).
弹飞绵羊的动态树解法:
1 //f: 辅助树中的父节点,若为辅助树中的根,则为实际树中的父节点 2 int f[N],son[N][2],val[N],sum[N],tmp[N];bool rev[N]; 3 bool isroot(int x){return !f[x]||son[f[x]][0]!=x&&son[f[x]][1]!=x;} 4 void rev1(int x){if(!x)return;swap(son[x][0],son[x][1]);rev[x]^=1;} 5 void pb(int x){if(rev[x])rev1(son[x][0]),rev1(son[x][1]),rev[x]=0;} 6 void up(int x){ 7 sum[x]=val[x]; 8 if(son[x][0]) sum[x]+=sum[son[x][0]]; 9 if(son[x][1]) sum[x]+=sum[son[x][1]]; 10 } 11 void rotate(int x){ 12 int y=f[x],w=son[y][1]==x; 13 son[y][w]=son[x][w^1]; 14 if(son[x][w^1]) f[son[x][w^1]]=y; 15 if(f[y]){ 16 int z=f[y]; 17 if(son[z][0]==y)son[z][0]=x; 18 else if(son[z][1]==y)son[z][1]=x; 19 } 20 f[x]=f[y];f[y]=x;son[x][w^1]=y;up(y); 21 } 22 void splay(int x){ 23 int s=1,i=x,y;tmp[1]=i; 24 while(!isroot(i))tmp[++s]=i=f[i]; 25 while(s)pb(tmp[s−−]); 26 while(!isroot(x)){ 27 y=f[x]; 28 if(!isroot(y)){if((son[f[y]][0]==y)^(son[y][0]==x))rotate(x);else rotate(y);} 29 rotate(x); 30 } 31 up(x); 32 } 33 void access(int x){for(int y=0;x;y=x,x=f[x])splay(x),son[x][1]=y,up(x);} 34 35 int root(int x){access(x);splay(x);while(son[x][0])x=son[x][0];return x;} 36 void makeroot(int x){access(x);splay(x);rev1(x);} 37 void link(int x,int y){makeroot(x);f[x]=y;access(x);} 38 void cutf(int x){access(x);splay(x);f[son[x][0]]=0;son[x][0]=0;up(x);} 39 void cut(int x,int y){makeroot(x);cutf(y);} 40 int ask(int x,int y){makeroot(x);access(y);splay(y);return sum[y];} 41 //附 42 int tot; 43 int newnode(){ 44 ++tot; 45 son[tot][0] = son[tot][1] = f[tot] = rev[tot] = 0; 46 val[tot] = sum[tot] = 0; 47 return tot; 48 }
弹飞绵羊的分块做法: