长链剖分学习笔记

洛谷日报

作为树链剖分中的一种,长链剖分远没有重链剖分和实链剖分常用,于是仅作简单了解。

长链和长儿子

一个点的所有儿子中能够引出深度最深的那个点为这个点的长儿子。由长儿子关系组成的链称为长链。

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)\) 继承重儿子信息(似乎 vectorswap 可以,没试过),于是可以直接让一条链的信息都维护在 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\) 的点的点对数。那么有:(大写为父亲,小写为儿子)

\[Ans \gets F(i)g(i+1)+G(i)f(i-1) \]

\[F(i) \gets f(i-1) \]

\[G(i) \gets F(i)f(i-1) + g(i+1) \]

直接暴力能做到 \(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 序。

posted @ 2020-09-10 20:22  JiaZP  阅读(153)  评论(0编辑  收藏  举报