之前写过关于树链剖分学习的相关笔记,那个笔记的内容就比较粗略,为了加深树链剖分的学习,在这里进行一次尽量完整地总结,静下心来,进行一次完整的学习
树链剖分本质上是利用两次dfs进行树上搜索来保存树上信息,利用这些信息从而可以让线段树或者树状数组这样区间操作很快的数据结构发挥作用!
为什么?
我们首先需要从树链剖分的思想开始入手:
树链剖分是将树结构转化为一个序列区间
为什么?
区间容易维护
怎么做?
就是利用dfs进行转化
那么我们的重点就是如何进行dfs,如何利用dfs保存的信息来建树。
在此之前,我们首先要开始探究树链剖分的定义:
fa(x)表示x在树上的父亲
siz(x)表示x子树的大小(节点数量)
son(x)表示x的重儿子(就是x的儿子中siz最大的子节点)
dep(x)表示x节点的深度
top(x)x所在链上的顶部端点
tid(x)和rnk(x)相互映射
tid(x)相当于树上节点->树链剖分成序列点序
rnk(x)相当于树列剖分的点序->树上节点
这里加一条就是再边权维护的时候,要将边映射到点上,进行剖分
就利用eid(x)->该边的端点
我们知道这些定义的变量之后,就可以去进行两次dfs了
第一次dfs目的是为了求fa、siz、son、dep这四个变量,只是求出来树上的每一个节点的信息
1 int dfs1(int u, int f) { 2 dep[u] = dep[f] + 1, siz[u] = 1, son[u] = 0, fa[u] = f; 3 for (int i = head[u]; i; i = next[i]) { 4 int v = ver[i]; 5 if (v == f) continue; 6 siz[u] += dfs1(v, u); 7 eid[(i-1) / 2 + 1] = v;//维护边权需要对边进行映射 8 if (siz[v] > siz[son[u]]) son[u] = v; 9 }10 return siz[u];11 }
第二次dfs目的是为了求剩下的变量
1 void dfs2(int u, int tp) { 2 top[u] = tp, tid[u] = ++pos, rnk[pos] = u; 3 if (!son[u]) return; 4 dfs2(son[u], tp); 5 for (int i = head[u]; i; i = next[i]) { 6 int v = ver[i]; 7 if (v == fa[u] || v == son[u]) continue; 8 dfs2(v, v); 9 }10 }
注意入口
1 dfs1(1,0);2 dfs2(1,1);
接下来想要建立维护变量的数据结构实际上就是对于剖分后的区间进行映射到你要维护的区间上就好了
关于如何求解路径上的一些答案
1 int linkquery(int u, int v) { 2 int ans = 0; 3 while (top[u] != top[v]) { 4 if (dep[top[u]] < dep[top[v]]) std::swap(u, v); 5 ans += ask(1, tid[top[u]], tid[u]); 6 u = fa[top[u]]; 7 } 8 if (u == v) return ans; 9 if (tid[v] < tid[u]) std::swap(u, v);10 ans += ask(1, tid[u] + 1, tid[v]);11 return ans;12 }
以上总结代码依赖于:
步骤与代码:
- dfs1(当前节点,当前节点父亲节点)
- dfs2(当前节点,端点)
- 构建数据结构
- 通过不断的跳转端点,从而query出答案来
一些细节:
- 因为深度取决于父亲节点,可以直接由父亲节点的dep推出
- son可以为0,这样不需要修改
- siz节点包括自身(子树包括自身)
- dfs2优先处理重儿子,让链尽量长,这样子链条总数为O(logN)
- 点权维护要有端点处理,边权因为映射到u->v的v上,所以少了一个点,也就是不维护端点
- 优先跳转深度浅的