树链剖分详解
Posted tle666
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了树链剖分详解相关的知识,希望对你有一定的参考价值。
食用之前请务必搞清楚线段树
什么是树链剖分
- 树链剖分,它可以对一棵树进行轻重链剖分后用数据结构来维护每条重链。
- 比如下面这个问题:假设每个点有一个点权。如何把一棵树上的两个点\(u\),\(v\)之间的简单路径上的所有点的点权增加\(d\)?
- 这就是树链剖分能够解决的的一个基本问题。
接下来介绍一下树链剖分的详细过程。
轻重链剖分
树链剖分的第一步就是将一棵树进行轻重链剖分。这一步决定了整个树链剖分的时间复杂度。
引入几个概念:
- \(size[u]\):以\(u\)为根的子树大小
- \(wson[u]\):在\(u\)的儿子中\(size\)值最大的那一个,称作\(u\)的重儿子
- \(dfn[u]\):每个点的dfs序号。\(pre[tot]\),如果dfn[u]=tot,则\(pre[tot]=u\)
- 重链:指每个点与它的重儿子之间的连边(\(u\)---\(wson[u]\))
轻链:在所有边中不是重的其他边
举个例子来详细说一下。
- 如上图
(一张从百度百科挖来的图),每一个带红点的点就是轻儿子;每一条加粗的的边就是重链,没有加粗的就是轻边。比如说对于点2,那么
\(wson[2]=6,size[2]=5,size[wson[u]]=size[6]=3\)。 - \(dfn[u]\)的含义就要稍微的特殊一些。因为它不仅仅是简单的dfs序号,而是按照轻重链的顺序来定义的。每次从一个点\(u\)往下dfs的时候,优先对\(wson[u]\)进行dfs,然后再遍历轻儿子们。
- 比如说上图,每条边上有一个序号。把这个序号看做是点的(比如说1->4上面的1应该是dfn[4])。从1开始dfs,可以明显的看到是先对4进行dfs,然后再对wson[4]也就是9进行dfs....到了叶子节点再回溯去dfs轻儿子。
- 为什么要这样做呢?因为这样之后,可以看到,每一条长重链上的每个点的dfn是连续的。比如1->4->9->13->14这条长重链上的dfn便是连续的,其他两条也一样。这样一来,就可以用维护区间的数据结构(比如线段树)来维护重链上的信息。
树链剖分的好处
- 一个上面已经说了,就是重链上的dfn是连续的,可以用数据结构维护
还有就是,在每一条由根到叶子结点的路径上,轻链的条数和重链的长度均不会超过\(\log n\)。这个性质决定了树链剖分的时间复杂度。如果是拿线段树来维护链的话,复杂度就是\(n \log^2n\)
代码实现轻重链剖分
- 一共需要两个dfs。
第一个用来处理每个点的基础信息(\(wson,size,dep,fa\))
第一个dfs代码:
inline dfs1(int u, int f)//两个参数:u是现在的点,f是u的父亲
{
size[u] = 1;//最开始u的子树中只有u一个点
for(edge *p = head[u]; p; p = p->next)//遍历每一个与u相连的点
{
int v = p->v;
if(v == f) continue;//如果此点是u的父亲就跳过
dep[v] = dep[u] + 1;
fa[v] = u;//处理信息
dfs1(v, u);//继续递归
size[u] += size[v];//u的子树大小要加上v的子树大小
if(size[wson[u]] < size[v]) wson[u] = v;//更新重儿子
}
}
- 这样就用一个dfs处理出了每个点的信息。
- 再用第二个dfs求出每个点的dfn以及该点所处的链的链首。
代码:
inline void dfs2(int u, int tp)//u是当前点,tp是该链链首
{
dfn[u] = ++tot, pre[tot] = u, top[u] = tp;
//pre是dfn的反函数。如dfn[2] = 3,pre[3] = 2...
//为什么要有这个反函数,因为在建线段树的时候是用dfn建的,然而如果想要知道原来那个点的信息就要知道pre
if(wson[u]) dfs2(wson[u], tp);//有重子就继续往下拉重链
for(edge * p = head[u]; p; p = p->next)//回过头来在轻儿子中拉链
{
int v = p->v;
if(v != fa[u] && v != wson[u]) dfs2(v, v);//如果不是爸爸或重儿子,已该点为链首继续拉链
}
}
至此,轻重链剖分完成,然后便是第二大块——
线段树维护重链
- 就是用线段树维护重链上的信息。
- 注意事项:这里所有的参数都是以dfn的形象出现的,而不是原来那个点的序号。
inline void build(node *r, int left, int right)
{
r->left = left, r->right = right, r->lazy = -1, r->s = 0;
if(left == right)
{
r->s = value[pre[left]];
//这里是pre[left]不是left!原因就是刚才说的:这里所有的参数都是以dfn的形象出现的,而不是原来那个点的序号。
return ;
}
int mid = (left + right) / 2;
node *lson = &pool[++cnt], *rson = &pool[++cnt];
r->ch[0] = lson, r->ch[1] = rson;
build(lson, left, mid); build(rson, mid + 1, right);
pushup(r);
}
基础的查询修改操作(在重链上的)就是原来的函数,怎么写详细请看线段树基础。
最后一步,也就是——
两点间路径的修改&查询
- 就是最开始提到的问题:如何把一棵树上的两个点\(u\),\(v\)之间的简单路径上的所有点的点权增加\(d\)?
上图
Q1:如果想要求11与2之间的和该怎样?
A1:在同一重链上,直接调用query查询就行了。
- 在同一重链上的两点都可以直接查询
Q2:求6与7之间的和?
A2:将6跳到1,同时答案加上query(dfn[2],dfn[6]);然后将7跳到1,答案加上query(dfn[3],dfn[7])
- 在不同重链上的两个点就一直按重链向上跳,直到跳到同一条重链上--->Q1
代码(修改的):
inline void modify(int u, int v, int d)
{
while(top[u] != top[v]) //直到跳到一条链上
{
if(dfn[top[u]] < dfn[top[v]]) swap(u, v);//这里根据链首的值交换一下
change(root, dfn[top[u]], dfn[u], d), u = fa[top[u]];//最后u=链首的爸爸
}
if(dep[u] > dep[v]) swap(u, v);
change(root, dfn[u], dfn[v], d);
//Q1
}
查询的(基本类似):
inline int Qsum(int u, int v)
{
int ret = 0;
while(top[u] != top[v])
{
if(dfn[top[u]] < dfn[top[v]]) swap(u, v);
ret += query(root, dfn[top[u]], dfn[u]), u = fa[top[u]];
}
if(dep[u] > dep[v]) swap(u, v);
ret += query(root, dfn[u], dfn[v]);
return ret;
}
至此基本完结
类模板题
CF343D,[NOI2015]软件包管理器,[SHOI2012]魔法树,[SDOI2011]染色,[ZJOI2008]树的统计
以上是关于树链剖分详解的主要内容,如果未能解决你的问题,请参考以下文章