top cluster 树分块学习笔记
参考资料:周欣《浅谈一类树分块的构建算法及其应用》、@negiizhao Top tree 相关东西的理论、用法和实现、lxl [Ynoi2018] Day2 题解、@zhylj 「学习笔记」基于 Top Cluster 分解的树分块算法。
基本概念
一个树簇(cluster)是树上的一个连通点集,有至多两个点与外界连接。这两个点称为界点(boundary node),簇中其余的点称为内点(internal node)。两个界点之间的路径称为簇路径(cluster path)。方便起见,本文设每个簇中必有两个界点,称一个簇中深度较浅的界点为上界点,较深的为下界点。
(图源:@negiizhao Top tree 相关东西的理论、用法和实现,下面那张图出处相同。)
如上图所示,簇是可合并的。只要不断地执行上面两个操作就可以将整棵树合并为一个簇。簇的合并过程会形成一个二叉树的结构,我们称这棵树为 top tree。
Top tree 本身的实现对于树分块来说不太重要。但我们可以借用 top cluster 的理论,建立一种比常见树分块具有更多优秀性质的树分块算法。能够做到对于一棵
- 不同簇的边集不相交。
- 一个簇的两个界点必为祖孙关系。
- 一个簇中的点,除界点外,其余点不会在其它簇中出现。即任意内点只可能属于一个簇。
- 如果一个点在多个簇中出现,那么它一定是某一个簇的下界点,同时是其余包含该点的簇的上界点。(根节点除外,因为它不可能是任何簇的下界点。)
- 如果把所有簇的界点作为点,每个簇的上界点向下界点连有向边,则会得到一棵有根树,称为收缩树。
以下是一个该算法的演示:
(图源:周欣《浅谈一类树分块的构建算法及其应用》)
其中图 1 为原树。图 2 为原树的一种划分,每个圈代表一个簇,彩色的边表示簇路径。图 3 为原树的收缩树。
如何实现这个算法?一个自然的想法是先建出 top tree,然后在 top tree 上截取子树。然而 top tree 的构建比较复杂,较难实现。实际上存在一种更加易于实现的静态构建算法。
算法过程
选取任意节点为根节点,并且强令根节点为一个界点。
从根节点开始 DFS,维护一个栈存储暂时还未归类的边(实际上存的是点,但代表的意义是连向其父亲的边)。当
为根节点。 有至少两个子树中存在界点。此时如果不使得 为界点则 所在簇将产生至少三个界点( 的某个祖先将成为该簇的上界点,而 的子树中的界点将均成为下界点。存在两个以上的下界点显然是不合法的),所以必须令 为界点。- 栈中剩余边(点)的数量大于
。
下面要解决的问题是:如何合适地将
的子树已用完。- 新加入一个子树将会使当前簇中有两个下界点。
- 新加入一个子树将会使当前簇的大小超过
。
全部 DFS 结束后,我们就能得到一种符合要求的划分方案。
正确性证明
显然上述算法能够保证每个簇的大小均为
对于第一部分:显然情况 1、3 发生的次数为
对于第二部分:显然情况 1、2 只与第一部分的发生次数有关。对于情况 3,考虑将每个发生这种情况时正在划分的簇和当前做到的子树配对。则每对的未归类边数和一定大于
综上所述,总的划分出簇的个数为
代码
// CL: 簇 BN: 界点 CT: 收缩树 CLP: 簇路径
int fa[maxn]; // 记录原树中的父亲
int CT_fa[maxn], near_CLP[maxn], up_BN[maxn], down_BN[maxn];
// CT_fa:每个界点在收缩树上的父亲。near_CLP:距离最近的簇路径上节点。
// up_BN:所属簇(若为界点,则其所属簇为其作为下界点时所属的簇)的上界点。down_BN:所属簇的下界点。
vector<int> BN, down_CL[maxn]; // BN:存储所有界点。down_CL:把整个簇中上界点以外的点存到下界点的 vector 中。
namespace TOP_CLUSTER {
// 放在 namespace 里面的变量是只有划分时才会用到的。
int cur_CL[maxn], cur_CL_cnt; // 存放当前簇的临时数组。
inline void add_CL(int u, int v) { // 新增一个以 u 为上界点,v 为下界点的簇。
if (!v) // 如果没有下界点,则可以任选簇中一个点作为下界点。
v = cur_CL[cur_CL_cnt];
CT_fa[v] = u, near_CLP[u] = u;
for (int r = v; r != u; r = fa[r])
near_CLP[r] = r; // 预处理簇路径上每个点的最近簇路径上节点为它自己。
for (int i = 1; i <= cur_CL_cnt; i++) {
int r = cur_CL[i], j;
up_BN[r] = u, down_BN[r] = v, down_CL[v].emplace_back(r);
for (j = r; !near_CLP[j]; j = fa[j])
; // 暴跳找最近簇路径上节点,因为按 DFS 序加入栈中所以这部分复杂度可以保证。
near_CLP[r] = near_CLP[j];
}
cur_CL_cnt = 0; // 记得把存放当前簇的临时数组长度清零。
}
// ST: 栈
int ST[maxn], ST_top, rec_ST_top[maxn]; // rec_stack_top:记录 DFS 到一个节点时的栈顶。
int waiting[maxn], rec_BN[maxn];
// waiting:长存不灭的 waiting,逐渐消逝的 AC。(划掉)暂时还未归类的边。rec_BN:记录子树中最浅界点。
inline void CL_partition(int u, int FA) { // DFS 函数
fa[u] = FA, rec_ST_top[u] = ST_top;
for (auto it = edge[u].begin(); it < edge[u].end(); it++)
if (it->to == FA) { // 先把父亲删了后面处理方便一点。
edge[u].erase(it);
break;
}
waiting[u] = 1;
int BN_cnt = 0; // 记录有界点的子树个数。
for (const Edge &v : edge[u]) {
ST[++ST_top] = v.to;
CL_partition(v.to, u);
waiting[u] += waiting[v.to], rec_BN[v.to] && (rec_BN[u] = rec_BN[v.to], BN_cnt++);
}
if (waiting[u] > B || BN_cnt > 1 || !FA) { // 符合出栈条件
waiting[u] = 0, rec_BN[u] = u, BN.emplace_back(u);
for (int i = 0, j = rec_ST_top[u] + 1, cnt = 0, cur_down = 0, v; i <= edge[u].size(); i++) {
// cnt:当前簇的大小。cur_down:当前簇的下界点。
v = (i == edge[u].size()) ? 0 : edge[u][i].to;
if (cnt + waiting[v] > B || (cur_down && rec_BN[v]) || !v) { // 已无法往当前簇中再加入一个子树。
for (; (j < rec_ST_top[v] || !v) && j <= ST_top; j++)
cur_CL[++cur_CL_cnt] = ST[j];
add_CL(u, cur_down), cnt = cur_down = 0;
}
cnt += waiting[v], rec_BN[v] && (cur_down = rec_BN[v]);
}
ST_top = rec_ST_top[u]; // 把栈中所有 u 的子树中的节点弹出。
// 坑点:别写糊了把这句话写到 if 外面去了。我因为这个调了好长时间。
}
}
} // namespace TOP_CLUSTER
例题
Problem:
给定一棵
共
Solution:
显然可以将问题转化为以下式子:
前一项可以通过前缀和简单求得,后一项看起来比较难求。
使用莫队算法。考虑 P4211 的经典结论,设
这样本题就解决了吗?实际上你会发现任何常见数据结构都会使 卡常。众所周知,莫队二离可以看做对一种数据结构进行
加入一个节点时,把这个节点到根的路径拆分成以下 3 部分:
- 该点到最近簇路径上节点的路径。
- 该点的最近簇路径上节点到该簇上界点的路径。
- 该簇上界点到根节点的路径。(均为簇路径,可以认为是该簇上界点到根节点在收缩树上的路径。)
维护
这样这道题就彻底解决了。时间复杂度
Core code:
这里给出本题修改以及查询部分的代码。
// 划分方式与上面相同,略。
// diff_dep[i] = dep[i] - dep[fa[i]], diff_CT_dep[i] = dep[i] - dep[CT_fa[i]];
unsigned val[maxn], val_CLP[maxn], tag[maxn], sum1[maxn], sum2[maxn];
inline void update(int x) {
const int up = up_BN[x], down = down_BN[x];
for (; x != near_CLP[x]; x = fa[x])
val[x] += diff_dep[x];
for (; x != up; x = fa[x])
val[x] += diff_dep[x], val_CLP[down] += diff_dep[x];
for (; CT_fa[x]; x = CT_fa[x])
val_CLP[x] += diff_CT_dep[x], tag[x]++;
for (int it : down_CL[down])
sum1[it] = (down_BN[fa[it]] == down ? sum1[fa[it]] : 0) + val[it];
for (int it : BN)
sum2[it] = sum2[CT_fa[it]] + val_CLP[it];
}
inline unsigned query(int x) {
const int up = up_BN[x], down = down_BN[x], near = near_CLP[x];
return sum1[x] + (dep[near] - dep[up]) * tag[down] + sum2[up];
}
相关题目
如果发现有问题请联系我,我会尽快修正。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· winform 绘制太阳,地球,月球 运作规律
· AI与.NET技术实操系列(五):向量存储与相似性搜索在 .NET 中的实现
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 上周热点回顾(3.3-3.9)