Kruskal重构树 (Kruskal Reconstruction Tree)
Kruskal重构树是一种在图论中用于解决特定问题的数据结构,它基于经典的Kruskal最小生成树算法构建。它并非直接用于求解最小生成树,而是通过重构原图的边,将边权信息“上移”到新生成的节点上,从而将路径上的边权最值问题转化为树上路径的节点最值问题,并能方便地处理连通性和LCA(最近公共祖先)相关的问题。
一、核心思想
- 边权转点权:在原图中,边有权值。Kruskal重构树将这些边权值“提升”为新引入的内部节点的点权。
- 构建二叉树结构:对于原图中的每一条边
(u, v, w),我们创建一个新的节点p,其点权为w。然后将p作为u和v在当前并查集中的“代表元”(根节点)的父节点。这样,p就连接了包含u和v的两个连通分量。 - 保持连通性信息:最终形成的是一棵满二叉树(每个非叶子节点都有两个子节点),叶子节点是原图的顶点,内部节点对应原图的边(按Kruskal顺序添加)。
- 性质:
- 最大生成树/最小生成树:如果按边权升序排序,则重构树对应最小生成树的连通过程;如果按边权降序排序,则对应最大生成树的连通过程。
- 路径最值:在重构树上,任意两个叶子节点
u和v之间的简单路径上的最大点权(内部节点的权值),就等于原图中从u到v的路径上边权的最大值(对于最大生成树版本)或最小值(对于最小生成树版本)。这得益于Kruskal算法的贪心性质。 - LCA与连通性:
u和v的LCA节点的点权,就是连接u和v所在连通分量时所用的那条边的权值。
二、应用场景
- 最小瓶颈路问题 (Minimum Bottleneck Path):求两点间路径上最大边权的最小值。使用边权升序构建的重构树,答案就是LCA的点权。
- 最大瓶颈路问题 (Maximum Bottleneck Path):求两点间路径上最小边权的最大值。使用边权降序构建的重构树,答案就是LCA的点权。
- 在线查询:预处理重构树和LCA后,可以在
O(log n)时间内回答任意两点间的瓶颈值查询。 - 可持久化并查集模拟:重构树隐式地记录了并查集的合并历史。
三、构建步骤 (以最大生成树版本为例)
- 初始化:
- 将原图的
n个顶点作为叶子节点,编号为1到n。 - 创建一个并查集,初始时每个顶点独立。
- 将所有边按边权降序排序。
- 设
idx = n(下一个新节点的编号)。
- 将原图的
- 遍历边:
- 对于每条边
(u, v, w),按排序后的顺序处理:- 在并查集中查找
u和v的根节点fu和fv。 - 如果
fu != fv(即不在同一连通分量),则:idx++,创建新节点idx。- 设置新节点的点权
val[idx] = w。 - 将
fu和fv的父节点都设置为idx。即parent[fu] = idx,parent[fv] = idx。 - 在并查集中合并
fu和fv。
- 在并查集中查找
- 对于每条边
- 完成:处理完所有边后,得到一棵包含
2n-1个节点的树(n个叶子,n-1个内部节点)。根节点是最后创建的那个节点。
注意:最小生成树版本只需将边按升序排序。
四、C++ 实现
下面是一个完整的C++实现,包含Kruskal重构树的构建、LCA预处理和查询。
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int MAXN = 200005; // 假设原图最多100000个点,重构树最多200000个点
const int LOG = 18; // log2(MAXN)
struct Edge {
int u, v, w;
Edge(int u = 0, int v = 0, int w = 0) : u(u), v(v), w(w) {}
bool operator<(const Edge& other) const {
return w > other.w; // 降序,构建最大生成树版本
// 如果要构建最小生成树版本,改为 return w < other.w;
}
};
vector<Edge> edges;
vector<int> tree[MAXN]; // 重构树的邻接表
int val[MAXN]; // 重构树节点的点权
int parent[MAXN]; // 并查集父节点
int depth[MAXN]; // LCA用的深度
int fa[MAXN][LOG]; // LCA倍增数组
// 并查集 Find
int find(int x) {
return parent[x] == x ? x : parent[x] = find(parent[x]);
}
// 重构树构建 (最大生成树版本)
void buildKruskalTree(int n, int m) {
// 初始化并查集
for (int i = 1; i <= 2 * n; ++i) {
parent[i] = i;
tree[i].clear();
}
sort(edges.begin(), edges.end());
int idx = n; // 下一个新节点的编号
for (const Edge& e : edges) {
int fu = find(e.u);
int fv = find(e.v);
if (fu != fv) {
idx++;
val[idx] = e.w; // 新节点的权值为边权
parent[fu] = idx;
parent[fv] = idx;
tree[idx].push_back(fu);
tree[idx].push_back(fv);
// 注意:这里不合并并查集的parent,因为重构树的parent已经建立
// 但我们仍需要并查集来判断连通性,所以需要显式合并
// 修正:并查集的合并应该独立于重构树的parent
// 重新设计:用另一个数组做并查集
}
}
// 上面的并查集逻辑有误,需要修正
}
// 修正版:使用独立的并查集
int dsu_parent[MAXN];
int dsu_find(int x) {
return dsu_parent[x] == x ? x : dsu_parent[x] = dsu_find(dsu_parent[x]);
}
void buildKruskalTreeCorrect(int n, int m) {
// 初始化
for (int i = 1; i <= n; ++i) {
dsu_parent[i] = i;
parent[i] = 0; // 重构树的parent,初始为0
tree[i].clear();
}
for (int i = n + 1; i <= 2 * n - 1; ++i) {
tree[i].clear();
parent[i] = 0;
}
sort(edges.begin(), edges.end());
int idx = n;
for (const Edge& e : edges) {
int fu = dsu_find(e.u);
int fv = dsu_find(e.v);
if (fu != fv) {
idx++;
val[idx] = e.w;
// 在重构树中连接
parent[fu] = idx;
parent[fv] = idx;
tree[idx].push_back(fu);
tree[idx].push_back(fv);
// 在并查集中合并
dsu_parent[fu] = fv; // 或 dsu_parent[fv] = fu
}
}
// 根节点是idx,但idx可能小于2n-1,如果图不连通
// 通常我们假设图是连通的,否则需要处理多个连通分量
}
// DFS建立LCA所需信息
void dfs(int u, int p, int d) {
depth[u] = d;
fa[u][0] = p;
for (int i = 1; i < LOG; ++i) {
if (fa[u][i-1] != 0) {
fa[u][i] = fa[fa[u][i-1]][i-1];
} else {
fa[u][i] = 0;
}
}
for (int v : tree[u]) {
if (v != p) {
dfs(v, u, d + 1);
}
}
}
// 预处理LCA
void initLCA(int root, int n) {
for (int i = 1; i <= 2 * n - 1; ++i) {
for (int j = 0; j < LOG; ++j) {
fa[i][j] = 0;
}
}
dfs(root, 0, 0);
}
// LCA查询
int lca(int u, int v) {
if (depth[u] < depth[v]) swap(u, v);
// 提升u到与v同一深度
int diff = depth[u] - depth[v];
for (int i = 0; i < LOG; ++i) {
if (diff & (1 << i)) {
u = fa[u][i];
}
}
if (u == v) return u;
for (int i = LOG - 1; i >= 0; --i) {
if (fa[u][i] != fa[v][i]) {
u = fa[u][i];
v = fa[v][i];
}
}
return fa[u][0];
}
// 查询u到v路径上的瓶颈值 (最大生成树版本: 最小边权的最大值)
int queryBottleneck(int u, int v) {
int l = lca(u, v);
return val[l]; // LCA节点的点权即为答案
}
int main() {
int n, m, q;
cout << "输入顶点数n, 边数m, 查询数q: ";
cin >> n >> m >> q;
edges.clear();
for (int i = 0; i < m; ++i) {
int u, v, w;
cin >> u >> v >> w;
edges.emplace_back(u, v, w);
}
buildKruskalTreeCorrect(n, m);
// 找到根节点 (parent为0的节点)
int root = 0;
for (int i = 1; i <= 2 * n - 1; ++i) {
if (parent[i] == 0) {
root = i;
break;
}
}
initLCA(root, n);
cout << "输入" << q << "个查询 (u, v): " << endl;
while (q--) {
int u, v;
cin >> u >> v;
int bottleneck = queryBottleneck(u, v);
cout << "从" << u << "到" << v << "的最小边权的最大值为: " << bottleneck << endl;
}
return 0;
}
五、代码说明
- 数据结构:
Edge: 存储边的信息。edges: 存储所有边。tree: 重构树的邻接表。val: 重构树节点的点权。dsu_parent: 并查集数组。parent: 重构树的父节点指针(用于找根)。depth,fa: 用于LCA倍增算法。
- 函数:
dsu_find: 并查集查找,带路径压缩。buildKruskalTreeCorrect: 正确构建重构树,使用独立的并查集。dfs: DFS遍历重构树,计算深度和初始化倍增数组。initLCA: 初始化LCA预处理。lca: 查询LCA。queryBottleneck: 查询瓶颈值。
六、复杂度分析
- 构建重构树:
O(m log m)(排序占主导)。 - LCA预处理:
O(n log n)。 - 单次查询:
O(log n)。
七、注意事项
- 图必须是连通的,否则无法形成一棵树。如果不连通,会得到一个森林,需要对每个连通分量单独处理。
- 节点编号:原图顶点
1~n,新节点n+1 ~ 2n-1。 - 本实现以最大生成树为例,求“最小边权的最大值”。若需求“最大边权的最小值”,需将边按升序排序。
- 代码假设顶点编号从1开始。
Kruskal重构树是一个非常巧妙的数据结构,它将图的问题转化为树的问题,结合LCA可以高效解决一类瓶颈路径查询问题。理解其构建过程和性质是掌握它的关键。
1148

被折叠的 条评论
为什么被折叠?



