Kruskal 重构树 学习笔记
一、算法引入
Kruskal重构树是一种基于Kruskal最小生成树算法衍生出的数据结构,主要用于解决图中两点间路径上边权最值相关的一类问题。
它的基本思想是:将原图的最小生成树中的边转化为点,构建一棵新树,把边权信息转换为点权信息。
例如,求两点之间的所有简单路径上最大边权的最小值(即最小瓶颈路问题),用Kruskal重构树可以高效解决。其核心思路是:在保证原图连通的前提下,尽量选择边权小的边,这正是Kruskal算法的思想。
二、定义与性质
2.1 基本定义
Kruskal重构树是在Kruskal算法运行过程中重新构建出的一棵树。原图的每一个节点对应重构树上的一个叶子节点,而每一个新添加的节点(虚拟节点)代表原图的一条边,其点权即为该边的边权。
2.2 核心性质
| 性质 | 说明 |
|---|---|
| 堆性质 | 重构树是一个小根堆或大根堆,由建树时边权的排序方式决定 |
| LCA性质 | LCA(u, v) 的权值代表原图中 u 到 v 路径上边权的极值 |
| 二叉树 | 重构树是一棵二叉树 |
| 节点数量 | 若原图有 n 个点,重构树有 2n-1 个节点 |
2.3 LCA性质的两种情形
升序建树(从小到大排序):
LCA(u, v) 的权值代表原图 u 到 v 路径上最大边权的最小值。由于边权越大的节点深度越小,在这棵树上 u 到 v 的路径就是原图上尽量沿着边权小的边走,而 LCA(u, v) 就是 u 到 v 路径上深度最小的节点。
降序建树(从大到小排序):
LCA(u, v) 的权值代表原图 u 到 v 路径上最小边权的最大值。证明与升序情况类似。
一句话总结: 升序建树求“最大值最小”,降序建树求“最小值最大”。
三、建树过程
3.1 算法步骤
- 初始化:将原图的 n 个节点各自作为独立集合,并查集指向自身。
- 排序:将所有边按边权排序(升序或降序决定重构树的性质)。
- 遍历边:依次遍历每条边
(u, v, w):- 找到 u 和 v 所在并查集的根
fu、fv - 若
fu != fv(不在同一集合):- 新建一个节点
node(编号++cnt),点权设为这条边的边权w - 连边:
node分别与fu、fv相连 - 更新并查集:
fa[fu] = fa[fv] = node
- 新建一个节点
- 找到 u 和 v 所在并查集的根
- 处理森林:遍历完所有边后,得到的是一棵或多棵树。若原图不连通,则建出的是森林。遍历每个节点,找到其并查集的根作为其所在树的根。
3.2 建树图示理解
在Kruskal算法过程中,每当一条边被加入最小生成树时,我们不直接连接两个端点,而是:
- 找到两个端点所在集合的根
fau、fav - 新建一个虚拟节点
node - 将
fau和fav的父亲设为node - 将
node的点权设为当前边的边权
这样最终得到的就是一棵Kruskal重构树。
四、代码模板
以下代码实现了Kruskal重构树的构建(升序建树):
const int MAXN = 200005; // 原图点数
const int MAXM = 200005; // 原图边数
struct Edge {
int u, v, dis;
} rem[MAXM];
int n, m, cnt; // cnt为当前节点总数,初始为n
int val[MAXN << 1]; // 点权
int fa[MAXN << 1]; // 并查集
vector<int> tree[MAXN << 1]; // 重构树的邻接表
bool cmp(Edge a, Edge b) {
return a.dis < b.dis; // 升序排序
}
int find(int x) {
return fa[x] == x ? x : fa[x] = find(fa[x]);
}
void kruskal() {
// 初始化并查集
for (int i = 1; i <= n; ++i) fa[i] = i;
cnt = n;
sort(rem + 1, rem + 1 + m, cmp);
for (int i = 1; i <= m; ++i) {
int fu = find(rem[i].u), fv = find(rem[i].v);
if (fu != fv) {
val[++cnt] = rem[i].dis; // 新建节点,点权为边权
fa[cnt] = cnt; // 新节点加入并查集
fa[fu] = fa[fv] = cnt; // 更新并查集
// 在重构树中连边
tree[cnt].push_back(fu);
tree[fu].push_back(cnt);
tree[cnt].push_back(fv);
tree[fv].push_back(cnt);
}
}
}
代码说明:
cnt初始为n,每合并一条边就新建一个节点,最终cnt = 2n - 1- 新节点的点权
val[node]设为当前边的边权 - 重构树以最后新建的节点为根
LCA预处理(用于查询)
建树完成后,通常需要预处理LCA以支持快速查询:
int f[MAXN << 1][20]; // 倍增数组
int dep[MAXN << 1]; // 深度
void dfs(int u, int father) {
dep[u] = dep[father] + 1;
f[u][0] = father;
for (int i = 1; i <= 19; ++i) {
f[u][i] = f[f[u][i-1]][i-1];
}
for (int v : tree[u]) {
if (v == father) continue;
dfs(v, u);
}
}
int lca(int u, int v) {
if (dep[u] < dep[v]) swap(u, v);
// 将u提升到与v同一深度
for (int i = 19; i >= 0; --i) {
if (dep[f[u][i]] >= dep[v]) u = f[u][i];
}
if (u == v) return u;
for (int i = 19; i >= 0; --i) {
if (f[u][i] != f[v][i]) {
u = f[u][i];
v = f[v][i];
}
}
return f[u][0];
}
查询时,val[lca(u, v)] 即为答案。
五、应用场景
5.1 最小瓶颈路问题
问题:求两点间所有路径中,最大边权的最小值。
解法:按边权升序建立Kruskal重构树,答案即为 val[LCA(u, v)]。
5.2 路径限制查询
问题:从某点出发,只能经过边权不超过某个值的边,最多能到达多少个点。
解法:在重构树上从该点向上倍增,找到最后一个点权不超过限制值的节点,该节点的子树大小即为答案。
5.3 动态连通性问题
Kruskal重构树可以结合树剖、线段树等静态树结构,高效处理动态路径权值问题。
5.4 复杂图论问题
如 [IOI2018] 狼人问题,利用最小生成树和最大生成树分别建两棵重构树,解决路径上的多重约束问题。
六、总结
Kruskal重构树的核心思想可以概括为:
- 边权转点权:将Kruskal算法中加入的每条边转化为一个新节点
- 堆性质:建树方式决定了大根堆/小根堆的性质
- LCA性质:原图两点路径上的边权极值 = 重构树上两点LCA的点权
这一数据结构的优势在于,将图上的路径极值问题转化为了树上的LCA查询问题,从而可以用倍增、树剖等成熟的树上算法高效求解。

浙公网安备 33010602011771号