树链剖分, 就是将树结构剖分成多个不相交的链结构, 再以此结合线段树等对链区间操作的方式以达成对树结构修改的目的, 共有三种
重链剖分 (常用) , 长链剖分 (不常用) , 实链剖分 (LCT使用)
首先是最常用的重链剖分 (O(logn))
例: 【AgOHの算法胡扯】dfs序与树链剖分_哔哩哔哩_bilibili
已知一棵树, 每个结点上含有一个值, 设法实现以下操作
1. 将树上 x 结点到 y 结点的最短路径上的所有结点的值加上 z
2. 求树上 x 结点到 y 结点的最短路劲上的所有结点的值之和
3. 将树上以 x 为根结点的子树的所有结点的值加上 z
4. 求树上以 x 为根结点的子树的所有结点的值之和
重链剖分中, 首先需要知道以下定义
dfs 序: 即 dfs 过程中遍历结点的顺序, 与欧拉序不同, dfs 序不会重复出现一个结点
时间戳: 即 dfs 第一次访问某个结点的"时间", 从 1 开始递加
重儿子: 即一个结点的所有子结点中, 大小 (以它为根节点的子树所拥有的结点个数) 最大的一个
轻儿子: 一个结点下除了重儿子以外的所有子结点
重链: 由一个轻儿子 (根节点也是轻儿子) 开始, 不断向下往重儿子方向连出的链
时间戳将一颗树的所有节点进行了连续化, 一个结点子树上的结点的时间戳, 一定大于这个结点自身的时间戳
在进行 dfs 的过程中, 优先向重链移动并记录时间戳, 则一条重链上的时间戳必然是连续的, 因此, 通过线段树对连续链区间操作的功能以实现所需的对树操作有了可行性
剖分过程分为两步
第一步, 先跑一遍 dfs , 标记以下内容
结点的父结点、重儿子、深度、大小
(因为需要先知道每个结点的重儿子是谁, 才能通过找到重链标记时间戳)
第二步, 再跑一遍 dfs , 标记以下内容
结点的时间戳、dfs 序、所处重链的顶部
因此每个结点可以写成如下形式, 另外还需一个时间戳计数器、一个 w 数组记录结点权值的 dfs 序以及一棵维护 w 数组的线段树
typedef struct no
{
int val; //权值
vector<int> sons; //子结点
int fa; //父节点
int dep; //深度
int bson; //重儿子
int siz; //大小
int top; //顶部
int dfn; //时间戳
}no;
int w[N]; //结点权值的dfs序
int tim = 0; //时间戳计数
no tr[N]; //树
//一个维护w数组的线段树
//......
接下来进行第一次 dfs
void dfs1(int u, int f) //u结点, 其父结点为f
{
tr[u].fa = f; //标记父节点
tr[u].dep = tr[f].dep + 1; //标记深度
tr[u].siz = 1; //初始化大小
int maxsize = -1; //临时变量用于找重儿子
int len = tr[u].sons.size();
for (int i = 0; i < len; i++) //遍历所有子结点
{
int v = tr[u].sons[i];
if (v == f) //无向树, 避免去到父节点
continue;
dfs1(v, u); //dfs子结点
tr[u].siz += tr[v].siz; //增加当前结点大小
if (tr[v].siz > maxsize) //寻找重儿子
{
maxsize = tr[u].siz;
tr[u].bson = v; //更新重儿子
}
}
}
接下来进行第二次 dfs
void dfs2(int u, int t) //u结点, 其顶结点为t
{
tr[u].dfn = ++tim; //更新时间戳后赋值
tr[u].top = t; //标记顶结点
w[tim] = tr[u].val; //给w数组对应位置赋值
int len = tr[u].sons.size();
if (!tr[u].bson) //若无重儿子, 则无子结点
return;
dfs2(tr[u].bson, t); //对重儿子执行顶结点仍为t的dfs
for (int i = 0; i < len; i++) //遍历所有子结点
{
int v = tr[u].sons[i];
if (v == tr[u].bson || v == tr[u].fa)//不包括重儿子以及父结点
continue;
dfs2(v, v); //对轻儿子执行顶结点为其自身的dfs
}
}
至此, 原树已被剖分存储在 w 数组中, 接下来使用线段树操作即可实现所需四种功能
//操作1, 将树上 x 结点到 y 结点的最短路径上的所有结点的值加上z(或赋值为z)
void mchain(int x, int y, int z)
{
while (tr[x].top != tr[y].top) //若不在同一条重链上
{
if (tr[tr[x].top].dep < tr[tr[y].top].dep) //令x为顶结点深度更深的结点(方便跳至另一条重链修改)
swap(x, y);
modify(tr[tr[x].top].dfn, tr[x].dfn, 1, z); //修改当前重链上一段区间的值
x = tr[tr[x].top].fa; //跳向上一条重链
}
if (tr[x].dfn > tr[y].dfn) //方便区间修改
swap(x, y);
modify(tr[x].dfn, tr[y].dfn, 1, z); //修改当前重链上一段区间的值
}
//操作2, 求树上 x 结点到 y 结点的最短路劲上的所有结点的值之和
int qchain(int x, int y)
{
int res = 0;
while (tr[x].top != tr[y].top) //大体逻辑与操作1一致
{
if (tr[tr[x].top].dep < tr[tr[y].top].dep)
swap(x, y);
res += query(tr[tr[x].top].dfn, tr[x].dfn, 1); //查询当前重链上一段区间的值
x = tr[tr[x].top].fa;
}
if (tr[x].dep > tr[y].dep)
swap(x, y);
res += query(tr[x].dfn, tr[y].dfn, 1); //查询当前重链上一段区间的值
return res;
}
//操作3, 将树上以x为根结点的子树的所有结点的值加上z(或赋值为z)
inline void mson(int x, int z)
{
modify(tr[x].dfn, tr[x].dfn + tr[x].siz - 1, 1, z);
}
//操作4, 求树上以x为根结点的子树的所有结点的值之和
inline int qson(int x)
{
return query(tr[x].dfn, tr[x].dfn + tr[x].siz - 1, 1);
}