树链剖分
树链剖分
序
我们知道,在序列上的算法有很多,比如应用广的线段树,可以解决简单问题的树状数组,有不带修改的RMQ问题ST等可树上的问题就很难解决了,因为树没有区间,那如果在u->v的简单路径上做一点操作,那岂不炸了于是有了树链剖分
第一步 思想
树链剖分,顾名思义,就是把数剖成一条一条链,然后用线段树解决,这个思路大家都能想到,可怎么高效(查询修改的时间是对的)剖就要好好思考了这时,一位伟人(不知道他是谁)提出了一个重要的思想重链剖分!!!
重链剖分
重链剖分的思想就是将除叶子结点,将自己的儿子分成重儿子和轻儿子重儿子就是子树最大的那个儿子,其余就是轻儿子。重链就是以一个非根的轻儿子然后递归他的重儿子直到叶子结点他牛逼在哪呢?他有一个重要的性质:在重链上的点的dfs序是连续的,其中,dfs序是你从1号点搜索,第几个搜到的,不带回溯那就可以了
第二步 预处理
重链剖分的题目都需要两个dfs, 这两个dfs就是要将重链剖分几个重要的数组给处理好
第一个dfs就是将dfn[i]:表示i的时间戳,zson[i]:i的重儿子,sz[u]:代表u的子树的节点个数,fa[u]:代表u的父亲,dep[u]:代表u的深度给预处理好
还有一个数组叫做tour[i],这个数组等我们讲到查询和修改的时候讲
第二个dfs就是将top[u]:表示u的重链顶给弄好
第一个dfs都很好求,主要是第二个dfs
第二个dfs就是除了u和fa,还要传一个ontop,就是看u是否是链顶,1明显是
然后就是将top[u]给做好,若ontop为1,那么top[u]=u,若ontop为0,则top[u]=top[fa[u]]
然后把重儿子先遍历,他肯定不是链顶,所以直接将ontop设为false,在传进去
其他儿子就直接设为true,传进去
好了,预处理就搞好了
放个代码:
void dfs1(int u, int faa){
sz[u] = 1;
fa[u] = faa;
dfn[u] = ++idx;
tour[idx] = u;//这个马上就来讲了
dep[u] = dep[faa] + 1;
int szson = 0;
for(auto v : edge[u]){
if(v == faa) continue;
dfs1(v, u);
sz[u] += sz[v];
if(sz[v] > szson){
szson = sz[v];
zson[u] = v;
}
}
}
void dfs2(int u, int faa, bool ontop){
if(ontop){
top[u] = u;
}else{
top[u] = top[faa];
}
if(zson[u] != 0)dfs2(zson[u], u, false);
for(auto v : edge[u]){
if(v != zson[u] && v != faa){
dfs2(v, u, true);
}
}
}
第三步 查询和修改
以下讲的都为点权,边权下放为点权即可
现在我们就是要讲一下tour数组的作用了
因为线段树是根据你的dfn数组干的,所以我们在一开始t[x]等于多少是就很麻烦了,因为我们不知道他这个是第几个点
所以我们需要tour数组来记录这个dfn对应的点,从而我们就可以轻松用build了
然后我们就开始讲在u-->v的简单路径上的查询+修改
修改永远就是这个操作:
俩个点,若他们不在同一条链上,我们操作
将u设为那个重链顶深度低(深度更大)的那个点
然后将它到链顶的这段路径修改
最后将跳完的俩个点的路径修改,结束!
查询就是将上面的操作修改,需要看题意了,若题目的查询是有顺序的,那么就要开始修改上面的板子,但没有固定的板子,若没有顺序就可以不改板子
重链剖分的最后一个忠告
若你的代码能力很低,请不要在写树链剖分的板子复制,粘贴,然后修改,提交,这会大大降低你的代码能力
所以最好不要复制,因为考场上并没有模版给你ctrl+c/v!!!
人们在拥有了一个重链剖分后,基本上就没人想还能怎么剖了
可当人们看到一个树形DP题后,就炸了
因为他的n为$10^5$,若直接DP就会炸掉
平常的DP优化就没有用了,比如单调队列,树状数组/线段树,前缀和,斜率优化,四边形决策优化,都失效了
这时,有时一位伟人出现了,他提出了第二种剖分,叫做长链剖分!!!
长链剖分
俗话说的好,要想弄明白一个算法,要知道他的1.思想,2.预处理,3.实现
第一步 思想
长链剖分,就是将重链剖分的重儿子和轻儿子换一个定义
重儿子:子节点中子树深度最大的子节点
轻儿子:除重儿子外的其他子节点
重儿子也可以叫做长儿子
预处理部分和重链剖分差不多,这里不细讲
也不给代码
第二三步 性质+实现
长链剖分有一个应用,就是求树上k级祖先
一般的想法就是一步一步向上跳,也能跳到k级
时间复杂度就为$O(mk)$了,时间不对
怎么办?
哦,这和LCA很像,可以直接用$O(n\log n)$的预处理+$O(m\log k)$的查询
很棒,时间对的,但不是重点
长链剖分有一个性质,就是一个点到根的距离轻边的切换条数为$O(\sqrt{n})$的
我们倍增求出$2i$级祖先,且$2i \le k \le 2^{i+1}$
那么我们将重链根据深度,把重链放入表格,然后就可以了
时间比类似LCA的倍增更优,$O(n\log n)-O(m)$的查询
现在回归主题,怎么用长链剖分优化DP?
现在看CF1009F
以下为OI-WIKI内容
我们设 𝑓𝑖,𝑗
表示在子树 i 内,和 i 距离为 j 的点数。
直接暴力转移时间复杂度为$ 𝑂(𝑛^2)$
我们考虑每次转移我们直接继承重儿子的 DP 数组和答案,并且考虑在此基础上进行更新。
首先我们需要将重儿子的 DP 数组前面插入一个元素 1, 这代表着当前结点。
然后我们将所有轻儿子的 DP 数组暴力和当前结点的 DP 数组合并。
注意到因为轻儿子的 DP 数组长度为轻儿子所在重链长度,而所有重链长度和为 𝑛
。
也就是说,我们直接暴力合并轻儿子的总时间复杂度为 𝑂(𝑛)
。
以下为作者写的内容
这个DP我们先将转移写出来,然后就直接可以发现可以套上长链剖分,可以优化,
长链剖分优化DP就是将儿子的答案搞一搞,变成现在的答案
最后就直接优化完成
以下参考OI-WIKI
#include <algorithm>
#include <iostream>
using namespace std;
constexpr int N = 1000005;
struct edge {
int to, next;
} e[N * 2];
int head[N], tot, n;
int d[N], fa[N], mx[N];
int *f[N], g[N], mxp[N];
int dfn[N];
void add(int x, int y) {
e[++tot] = edge{y, head[x]};
head[x] = tot;
}
void dfs1(int x) { // 第一次插入一个1
d[x] = 1;
for (int i = head[x]; i; i = e[i].next)
if (e[i].to != fa[x]) {
fa[e[i].to] = x;
dfs1(e[i].to);
d[x] = max(d[x], d[e[i].to] + 1);
if (d[e[i].to] > d[mx[x]]) mx[x] = e[i].to;
}
}
void dfs2(int x) { // 第二次合并
dfn[x] = ++*dfn;
f[x] = g + dfn[x];
if (mx[x]) dfs2(mx[x]);
for (int i = head[x]; i; i = e[i].next)
if (e[i].to != fa[x] && e[i].to != mx[x]) dfs2(e[i].to);
}
void getans(int x) { // 暴力合并算答案
if (mx[x]) {
getans(mx[x]);
mxp[x] = mxp[mx[x]] + 1;
}
f[x][0] = 1;
if (f[x][mxp[x]] <= 1) mxp[x] = 0;
for (int i = head[x]; i; i = e[i].next)
if (e[i].to != fa[x] && e[i].to != mx[x]) {
getans(e[i].to);
int len = d[e[i].to];
for (int j = 0; j <= len - 1; j++) {
f[x][j + 1] += f[e[i].to][j];
if (f[x][j + 1] > f[x][mxp[x]]) mxp[x] = j + 1;
if (f[x][j + 1] == f[x][mxp[x]] && j + 1 < mxp[x]) mxp[x] = j + 1;
}
}
}
int main() {
cin.tie(nullptr)->sync_with_stdio(false);
cin >> n;
for (int i = 1; i < n; i++) {
int x, y;
cin >> x >> y;
add(x, y);
add(y, x);
}
dfs1(1);
dfs2(1);
getans(1);
for (int i = 1; i <= n; i++) cout << mxp[i] << '\n';
}
好了,现在俩大主树链剖分已经讲完了
最后打上字幕把
作者:LQY
参考:OI-WIKI
浙公网安备 33010602011771号