这几天学习了一下树链剖分,个人感觉是线段树在树上的应用,也不能算是一种数据结构吧。比较常见的是对树边的修改和查询问题。
问题引入:
如果题目是在一棵树上进行路径的修改、求极值、求和等问题,大家很容易就会想到用线段树来做,但是这就存在一个问题:线段树存储的是一段连续区间上的信息,,而树的结构是分散的,那么怎么把树的节点或者树边和这个区间联系起来呢?树链剖分就可以解决这个问题。
首先,我们来解读一下算法名称:
1、树链,就是树上的路径。
2、剖分,就是把路径分类为重链和轻链。
树链剖分用一句话概括就是:把一棵树剖分为若干条链,然后利用数据结构(树状数组,SBT,Splay,线段树等等)去维护每一条链以解决问题,算法复杂度为O(logn)。
这里需要几个数组来保存树的信息:
size[u] 表示以 u 为根的子树的节点数
dep[u] 表示 u 的深度(根深度为1)
top[u] 表示 u 所在的链的顶端节点
fa[u] 表示u的父亲节点
son[u] 表示与u在同一重链上的u的儿子节点(姑且称为重儿子)
id[u] 表示u与其父亲节点的连边(姑且称为u的父边)在线段树中的位置
rank[i] 表示线段树中第 i 个数对应树中的节点编号
涉及到的几个概念:
重儿子:size[v]为u的子节点中size值最大的,那么v就是u的重儿子。
轻儿子:u的其它子节点。
重边:点u与其重儿子的连边。
轻边:点u与其轻儿子的连边。
重链:由重边连成的路径。
轻链:轻边。
剖分后的树有如下性质:
性质1:如果(u,v)为轻边,则size[v] * 2 < size[u];
性质2:从根到某一点的路径上轻链、重链的个数都不大于logn。
算法实现:
一、首先,我们可以对树进行两遍 dfs 求得以上数组的值
第一次dfs:求出 dep, size, fa, son,记录所有的重边
1 void dfs_1( int u, int f, int d) //主函数中调用dfs(1,0,1) 2 { 3 dep[u] = d; 4 size[u] = 1; 5 fa[u] = f; 6 for(int i = head[u];i != -1;i = edge[i].next) 7 { 8 int v = edge[i].to; 9 if(v == f) 10 continue; 11 dfs_1( v, u, d+1); 12 size[u] += size[v]; 13 if(son[u] == -1 || size[son[u]] < size[v]) 14 son[u] = v; 15 } 16 }
第二次dfs:连接重边成重链
1 void dfs_2( int u, int tp) //主函数中调用dfs(1,1) 2 { 3 top[u] = tp; 4 id[u] = ++tmp; 5 rank[tmp] = u; 6 if(son[u] != -1) 7 dfs_2( son[u], tp); 8 for(int i = head[u];i != -1;i = edge[i].next) 9 { 10 int v = edge[i].to; 11 if(v != fa[u] && v != son[u]) 12 dfs_2( v, v); 13 } 14 }
二、然后,建立线段树。将树中的权值在线段树中更新,这样建链和建线段树就完成了。具体实现可以看相关题解。
三、最后,就是修改和查询操作。单点的修改和查询很简单,只要将对应线段树中位置的信息修改或查询即可。重点要讲的是区间上的修改和查询,这两个操作写法类似,以修改为例。
如何修改u到v的边权的值呢?这里有两种情况:
(1)如果u与v在同一条重链上,那么就直接修改了
(2)如果u与v不在同一条重链上,那么就一边进行修改,一边将u与v往同一条重链上靠,这样就变成了第一种情况了
这样问题的关键就变成了如何将 u 和 v 往同一条重链上靠。
具体解法如下:
我们记 tp1 = top[u],tp2 = top[v]
(1) 当tp1 != tp2 时:不妨设dep[tp1] >= dep[tp2],那么就更新u到tp1的父边的权值(logn),并使u = fa[tp1]。
(2)当tp1 = tp2时:u与v在同一条重链上,若u与v不是同一点,就更新u到v路径上的边的权值(logn),否则修改完成;
重复上述过程,直到修改完成。
算法正确性证明:
首先我们需要深入理解一下dfs_2 。可以看出,dfs_2的核心是将树按一条条重链保存到线段树中。在树中同一条重链的节点或边的信息在线段树中是连续的,而且是深度小的保存在前面,深度大的保存在后面,这样线段树中保存的是一段段重链的信息。
如果需要修改 u 到 v 的信息,如果u和v不在同一条重链上,我们必定需要分别修改u 和 v 到其所在重链的头结点的信息,重复下去最终 u 到 v 的修改会演变成同一条重链上两个节点间的修改。算法证毕。
区间修改操作举例代码:
1 void change(int u, int v, int value) 2 { 3 int tp1 = top[u], tp2 = top[v]; 4 while(tp1 != tp2) 5 { 6 if(dep[tp1] < dep[tp2]) 7 { 8 swap( tp1, tp2); 9 swap( u, v); 10 } 11 update(1, id[tp1], id[u], value); 12 u = fa[tp1]; 13 tp1 = top[u]; 14 } 15 if(u == v) 16 return; 17 if(dep[u] > dep[v]) 18 swap( u, v); 19 update(1, id[u], id[v], value); 20 }
区间查询操作举例代码:
1 int find( int u, int v) 2 { 3 int tp1 = top[u], tp2 = top[v]; 4 int res = 0; 5 while(tp1 != tp2) 6 { 7 if(dep[tp1] < dep[tp2]) 8 { 9 swap( tp1, tp2); 10 swap( u, v); 11 } 12 res = max(res, query( 1, id[tp1], id[u])); 13 u = fa[tp1]; 14 tp1 = top[u]; 15 } 16 if(u == v) 17 return res; 18 if(dep[u] > dep[v]) 19 swap( u, v); 20 res = max( res, query(1, id[son[u]], id[v])); 21 return res; 22 }
以上都是针对树边操作的树链剖分,也有对树的节点进行修改查询的题目,方法类似,只需要在理解的时候改变一下 id 数组的含义:id[ u ]表示 u 节点在线段树中的位置。
当然,如果是对节点的修改和查询,我们就可以用树状数组而不是线段树来保存信息,这样代码会简单很多。
举例: