长链剖分学习笔记
作为树链剖分中的一种,长链剖分远没有重链剖分和实链剖分常用,于是仅作简单了解。
长链和长儿子
一个点的所有儿子中能够引出深度最深的那个点为这个点的长儿子。由长儿子关系组成的链称为长链。
void dfs_son(int cur, int faa) {
dep[cur] = dep[faa] + 1;
int mx = -1;
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to; dfs_son(to, cur);
if (mxlen[to] > mx) mx = mxlen[to], son[cur] = to;
}
mxlen[cur] = mx + 1;
}
void dfs_chain(int cur, int topp) {
top[cur] = topp;
if (!son[cur]) return ;
dfs_chain(son[cur], topp);
for (register int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to; if (to == son[cur]) continue;
dfs_chain(to, to);
}
}
性质
一个点到根的路径的虚边数为 \(O(\sqrt n)\)。
(证明可以考虑经过一条虚边就加了个 \(len\) 的链,最劣时子树大小为一等差数列)
长链剖分的 DFS 序
与重链剖分类似,长链剖分也能在 dfn 上搞些事情,这集中体现在开空间上。如果每个点都开深度大小的空间,时空复杂度爆炸,无法做到 \(O(1)\) 继承重儿子信息(似乎 vector
的 swap
可以,没试过),于是可以直接让一条链的信息都维护在 DFS 序上的一个固定的区间中,方便直接继承。
当然用指针也可以。
应用
树上 k 级祖先
众所周知,倍增可以在线解决树上 \(k\) 级祖先的问题,复杂度为 \(O((n + q)\log n)\)。
还有一种离线的方法,把询问挂点上,DFS 树并记录当前链,查询就直接在栈上查。复杂度 \(O(n+q)\)
然后到了树链剖分显神威的地方了。重链剖分能够做到在线 \(O(n + qlogn)\)。具体来说就是如果发现可以跳链就跳,如果目标在链上就直接在 DFS 序上查。
长链剖分能做到在线 \(O(nlogn + q)\)。先长链剖分,每条链用 vector 记录链上的 \(len\) 个点以及链上方的 \(len\) 个点。顺便求出倍增数组。每次询问首先跳到 \(log_2L\) 级祖先,然后在当前链中查询目标点。当前链一定储存目标点,因为当前链深度至少是 \(L/2\)。
Dominant Indices
显然可以用 dsu on tree 来做,但是用长链剖分可以做到 \(O(n)\)(尽管非常慢)
仍然类似 dsu on tree 的做法,只不过这次先去长儿子,然后继承长儿子的信息,暴力合并其他儿子的信息。暴力继承是不行的,我的做法是直接记录在长链的顶点上。
每条链最多只会被合并一次,因此复杂度为 \(O(n)\)。
重链剖分不行,因为可能轻儿子比重儿子更深,导致重儿子的记录的深度被迫加长。
HOT-hotel
可以称作长链剖分优化DP以及动态开空间的模板了。
设 \(f(p)(i)\) 表示 \(p\) 节点子树中距离 \(p\) 为 \(i\) 的节点个数,\(g(p)(i)\) 表示 \(p\) 子树中已经选好两个点,且需要从子树外找一个距离 \(p\) 为 \(i\) 的点的点对数。那么有:(大写为父亲,小写为儿子)
直接暴力能做到 \(O(n^2)\)。
发现 DP 数组下标有关深度,那么一个套路就是用长链剖分来优化。重儿子直接继承,轻儿子暴力合并。需要在 dfn 上开数组,并且由于涉及到数组的左移右移,可能需要开二倍(一个点占用两个位置)。
关键代码:
int dfn[N], dcnt, top[N];
void dfs_chain(int cur, int topp) {
top[cur] = topp;
++dcnt; dfn[cur] = ++dcnt;
stf[topp] = dfn[topp] + mxl[topp], stg[topp] = dfn[topp];
...
}
void dfs(int cur) {
if (!son[cur]) {
f[stf[top[cur]]] = 1;
return ;
}
for (int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to; if (to != fa[cur]) dfs(to);
}
ans += g[stg[top[son[cur]]] + 1];
f[--stf[top[cur]]] = 1;
++stg[top[cur]];
for (int i = head[cur]; i; i = e[i].nxt) {
int to = e[i].to; if (to == fa[cur] || to == son[cur]) continue;
for (int j = 0; j <= mxl[to]; ++j)
ans += 1ll * f[stf[top[cur]] + j] * g[stg[to] + j + 1] + 1ll * g[stg[top[cur]] + j] * (j ? f[stf[to] + j - 1] : 0);
for (int j = 0; j <= mxl[to]; ++j) g[stg[top[cur]] + j] += g[stg[to] + j + 1];
for (int j = 1; j <= mxl[to]; ++j) g[stg[top[cur]] + j] += 1ll * f[stf[top[cur]] + j] * f[stf[to] + j - 1];
for (int j = 1; j <= mxl[to]; ++j) f[stf[top[cur]] + j] += f[stf[to] + j - 1];
}
}
WC2010 重建计划
这里长链剖分的套路越来越像重链剖分了。
发现DP数组的转移又是左移右移,上 DFS 序;查询时需要查区间最值,上线段树维护 DFS 序。