[学习笔记]树链剖分急速入门
前置知识
dfs序
很多树上问题通过dfs序来转换为序列
线段树
它是个好东西,或者其他数据结构
基本概念
树链剖分通过轻重边剖分将树剖分成多条链,然后利用数据结构来维护这些链
-
重儿子:父亲节点的所有儿子中子树结点数目最多(size最大)的结点;
-
轻儿子:父亲节点中除了重儿子以外的儿子;
-
重边:父亲结点和重儿子连成的边;
-
轻边:父亲节点和轻儿子连成的边;
-
重链:由多条重边连接而成的路径;
-
轻链:由多条轻边连接而成的路径
变量声明
int fa[N];//保存当前节点的父亲
int dep[N];//保存当前节点深度
int siz[N];//保存以当前节点为根的子树大小
int son[N];//保存重儿子
int top[N];//保存当前节点所在链的顶端节点
int dfn[N];//保存当前dfs标号在树中所对应的节点
int id[N];//保存树中每个节点剖分以后的新编号(DFS的执行顺序)
具体实现
我们通过两遍dfs来预处理上面声明的数组
dfs1
求出\(fa\),\(dep\),\(siz\), \(son\)
void dfs1(int p, int from)
{
siz[p] = 1, dep[p] = dep[from] + 1, fa[p] = from;
for (int i = f[p]; i; i = e[i].nx)
{
if (e[i].to == from) continue;
dfs1(e[i].to, p);
siz[p] += siz[e[i].to];
if (siz[e[i].to] > siz[son[p]]) son[p] = e[i].to;
}
}
dfs2
求出\(dfn\),\(id\),\(top\)
void dfs2(int p, int tp)
{
dfn[++len] = p, id[p] = len, top[p] = tp;
if (son[p]) dfs2(son[p], tp);//注意这里我们为了保证重链dfs序连续,我们先进入重儿子
for (int i = f[p]; i; i = e[i].nx)
if (e[i].to != fa[p] && e[i].to != son[p]) dfs2(e[i].to, e[i].to);
}
具体操作
引入问题:
给你一棵树,支持两种操作:
- 修改:将树从x到y结点最短路径上所有节点的值都加上z;
- 查询:求树从x到y结点最短路径上所有节点的值之和。
我们树剖之后,把两点之间的路径分成线段树可以维护的若干段,把这些段加起来便可以得到答案,修改同理
inline LL query(int u, int v)
{
LL ans = 0;
while (top[u] != top[v]) //如果两点在同一条重链上,就可以直接一次查询完毕了
{
if (dep[top[u]] < dep[top[v]]) std::swap(u, v);//链顶深度大的往上跳
ans = ask(1, id[top[u]], id[u]); //ask是线段树的查询区间和函数
u = fa[top[u]];
}
if (dep[u] < dep[v]) std::swap(u, v);
return ans + ask(1, id[v], id[u]);
}
类似的我们可以用树剖求LCA或者更改两点路径上的值
时间复杂度
树链剖分的两个显然的性质:
- 如果\((u, v)\)是一条轻边,那么\(size(v) < size(u)/2\);
- 从根结点到任意结点的路所经过的轻重链的个数必定都小于\(logn\);
可以证明,树链剖分的时间复杂度为\(O(nlog^2n)\)
题目练习
t[p].lc = t[p << 1].lc, t[p].rc = t[p << 1 | 1].rc;
t[p].s = t[p << 1].s + t[p << 1 | 1].s;
if (t[p << 1].rc == t[p << 1 | 1].lc) --t[p].s;
还要注意注意在我们跳链时,假如当前id[top[u]]的颜色与id[fa[top[u]]]的颜色相同,我们要记得给答案减1,因为它们是同种颜色属于同一段而我们把这段切成两段加了两次