树链剖分可以处理树上的区间问题,就是对一棵树分成几条链,把树形变为线性,减少处理难度,顺便还可以用到一些处理线性问题的数据结构,如线段树,树状数组啊等等。
这样我们就可以把一些很难在树上处理,或者根本就不能在树上处理的问题变得简单。
思想:
思想还是很简单的,就是数学上的转化思想,把不会处理的东西想办法转化成容易处理的问题
我们以洛谷P3384 树链剖分的模板来看,
这个题一共有四问,我们首先需要明确一些概念,以便理解树链剖分的主要形式。
- 重儿子:对于每一个非叶子节点,它的儿子中 儿子数量最多的那一个儿子 为该节点的重儿子
- 轻儿子:对于每一个非叶子节点,它的儿子中 非重儿子 的剩下所有儿子即为轻儿子
- 叶子节点没有重儿子也没有轻儿子(因为它没有儿子。。)
- 重边:连接任意两个重儿子的边叫做重边
- 轻边:剩下的即为轻边
- 重链:相邻重边连起来的 连接一条重儿子 的链叫重链
- 对于叶子节点,若其为轻儿子,则有一条以自己为起点的长度为1的链
- 每一条重链以轻儿子为起点。
然后我们再明确几个数组:
- fa:对于每一个节点他的父亲。
- deep:每一个节点的深度。
- size:这个节点的子树大小
- son:这个节点的重儿子
- top:每一个节点所对应的链的深度最低的节点。
- sp :这个节点的原权值
- ep:这个节点的在线段树中的权值
- id:这个节点在线段树中的编号
然后我们就可以愉快的树剖了。
树剖的前提条件当然是要预处理出所有的数组的值了。
我们用两个函数分别是dfs1, dfs2分别来预处理前5个数组的值和后4个数组的值。
代码:
inline void dfs1(long long u, long long f, long long d)//u是当前节点,f是当前节点的父亲, d是当前深度。 { deep[u] = d;//分别赋值 fa[u] = f;。 size[u] = 1;//因为子树大小要加上本身所以要初始化为1 long long numson = -1;//重儿子的子树大小 for(long long i = lin[u]; i; i = e[i].next)//遍历他的每个儿子 { long long to = e[i].to; if(to == f)//如果遍历到了父亲,跳过 continue; dfs1(to, u, d + 1);//先递归处理出所有没有求出的所有儿子的值。 size[u] += size[to];//把他的子树大小加上求出的儿子的子树大小 if(size[to] > numson)//如果该子树大小比他的重儿子的子树大小要大,那他就是当前的重儿子 { numson = size[to]; son[u] = to; } } } void dfs2(long long u, long long t)//u是当前节点,t是u所在链上的头部 { id[u] = ++tot;//分别赋值 ep[tot] = sp[u]; top[u] = t; if(!son[u])//如果没有重儿子返回 return; dfs2(son[u], t); for(long long i = lin[u]; i; i = e[i].next) { long long to = e[i].to; if(to == fa[u] || to == son[u])除去重儿子的所有情况 continue; dfs2(to, to);//把每个轻儿子都作为链的顶端开始从轻儿子向下递归。 } }
预处理完之后我们在慢慢分别实现四个操作。
操作1: 让我们把x到y上最短路径上的所有节点都加上z。
这个我们先想暴力的做法,暴力的话就一个一个把x到lca(x,y)和lca(x,y)到y上面的所有节点的点权都加上z。很明显时间复杂度是线性的。这是我们能注意到我们要处理的这些节点在线段树中都是连续的,所以可以用线段树来优化使得时间能达到线段树的级别。
该怎么做呢,应该先把x设置为深度更深的那个点,然后判断是否在同一个链上,如果不是一个链上时,就调整深度更深的点(x)让x跳到他所在的链的顶端,调整的时候顺便把线段树中他所在链的顶端的位置到他的位置进行区间修改,最后在一个链上时再修改一遍。
操作2: 查询x到y上最短路径的节点和。
和上文一样,只是把区间修改改成区间查询,别忘了取模。
代码:
void uplca(long long x, long long y, long long k) { k %= p; while(top[x] != top[y]) { if(deep[top[x]] < deep[top[y]]) swap(x, y); update(id[top[x]], id[x], k, 1, n, 1); x = fa[top[x]]; } if(deep[x] > deep[y]) swap(x, y); update(id[x], id[y], k, 1, n, 1); } long long qulca(long long x, long long y) { long long sum = 0; while(top[x] != top[y]) { if(deep[top[x]] < deep[top[y]]) swap(x, y); long long res = 0; res += query(id[top[x]], id[x], 1, n, 1); sum += res; sum %= p; x = fa[top[x]]; } if(deep[x] > deep[y]) swap(x, y); long long res = 0; res += query(id[x], id[y], 1, n, 1); sum = (res + sum) % p; return sum % p; }
操作3:把节点x为根节点所在的子树的所有值都加上z。
这个反倒简单,因为我们已经记录了子树大小,又因为x到x的子树之间所有节点在线段树中都是连着的,且x是这段区间的起点,而区间长度是子树大小-1,就可以直接把这段区间修改。
操作4:查询以x为根节点的子树的所有点值。
这个同上。
代码:
inline void upson(long long x, long long k) { update(id[x], id[x] + size[x] - 1, k, 1, n, 1); } long long qson(long long x) { long long res = 0; res += query(id[x], id[x] + size[x] - 1, 1, n, 1); return res; }