edu47 F(长链剖分+树上启发式合并 / 线段树合并)
F. Dominant Indices
一道能巩固两种常用算法的好题。
Q:给定 \(n\) 个结点,根为 \(1\) 的树,求每个结点的子树中含有结点数量最多的最小深度。
长链剖分 + 树上启发式合并
对于求解每个子树答案的题,不难想到树上启发式合并。对于本题,常见的想法是:利用树上启发式合并维护每棵子树以结点深度为下标的权值线段树,然后再用线段树上二分去求最左侧最大值的位置。然而这样做的复杂度是 \(O(n \log^{2}n)\) 的,本题的 \(n\) 最大可达 \(10^{6}\),显然不可行。
核心原因在于在每次计算子树答案时需要对子树内每个结点分别做一次修改操作,总共需要进行 \(O(n\log n)\) 次操作,而每次操作是线段树的修改,为 \(O(\log n)\)。而我们会发现,对于每棵子树,深度相同的结点只会对线段树上的同一位置产生贡献,那么我们不妨在统计贡献时,对深度相同的所有结点一并统计。这样,对于每棵子树只需要做 \(O(子树高度)\) 次修改操作。
故我们考虑将重链剖分改成长链剖分。由于一条长链是由一个轻儿子加上若干连续的重儿子构成,在子树合并前会继承所有重儿子的状态,对于每一条长链,只会在枚举链头(轻儿子)时被额外计算一次,且修改次数恰好为该长链的长度(对应子树高度)。而所有长链的总长度为 \(n\),故总复杂度是 \(O(n)\) 的。配合线段树后,总复杂度为 \(O(n\log n)\);也可以直接利用长链剖分优化 dp \(O(n)\) 求解。
这里使用重链剖分与长链剖分是有区别的。证明:
如上图,若采用重链剖分计算图中子树的答案,显然会漏掉一些更深的深度,而采用长链剖分得到的长链一定能贯穿到所有深度。
长链剖分相关博客:blog
具体实现:利用指针+公共内存区域的方式,而不采用 vector。这样实现的技巧性非常强,但是会使代码的可读性大大提高。
对于每一条长链,以链头作开头,整条链按照 dfn 序(即从上到下)享有一段连续的空间。这样就将整个内存空间按照重链不同划分成了若干个连续段。在每棵子树 \(u\) 上需要进行的状态转移:(先转移重儿子,再依次转移所有轻儿子)
实现思路无比精妙,但感觉以后也很难复用成功。先记录着吧qwq。。。
int buf[N], *dp[N], k[N];
void dfs2(int p, int tp) {
dfsn[p] = ++cnt;
top[p] = tp;
node[cnt] = p;
dp[p] = buf + cnt; // dp[p]指向下标从dfsn[p]开始、长度为len[p]的一片区域
if (hson[p]) dfs2(hson[p], tp);
for (auto q : edges[p])
if (!top[q])
dfs2(q, q);
mdfsn[p] = cnt;
}
void dfs3(int p, int fa = 0) {
if (hson[p]) {
dfs3(hson[p], p);
k[p] = k[hson[p]] + 1;
}
dp[p][0] = 1;
if (dp[p][k[p]] <= 1) k[p] = 0;
for (auto q : edges[p])
if (q != fa && q != hson[p]) {
dfs3(q, p);
for (int i = 1; i <= len[q]; ++i) {
dp[p][i] += dp[q][i - 1];
if (dp[p][i] > dp[p][k[p]] || dp[p][i] == dp[p][k[p]] && i < k[p])
k[p] = i;
}
}
}
线段树合并
学过线段树合并就会发现这是一道板子题。只需要建出关于每棵子树中结点深度的桶的线段树(维护区间最大值),然后线段树上二分查询最左侧最大值的位置。
dfs 到某个结点 \(u\) 时,先将 \(u\) 的信息单独加入到 \(root[u]\) 版本的线段树中,再递归建好所有子树的线段树后,暴力将它们合并起来,得到 \(u\) 子树对应的线段树。总复杂度 \(O(n\log n)\)。


浙公网安备 33010602011771号