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 算法步骤

  1. 初始化:将原图的 n 个节点各自作为独立集合,并查集指向自身。
  2. 排序:将所有边按边权排序(升序或降序决定重构树的性质)。
  3. 遍历边:依次遍历每条边 (u, v, w)
    • 找到 u 和 v 所在并查集的根 fufv
    • fu != fv(不在同一集合):
      • 新建一个节点 node(编号 ++cnt),点权设为这条边的边权 w
      • 连边:node 分别与 fufv 相连
      • 更新并查集:fa[fu] = fa[fv] = node
  4. 处理森林:遍历完所有边后,得到的是一棵或多棵树。若原图不连通,则建出的是森林。遍历每个节点,找到其并查集的根作为其所在树的根。

3.2 建树图示理解

在Kruskal算法过程中,每当一条边被加入最小生成树时,我们不直接连接两个端点,而是:

  1. 找到两个端点所在集合的根 faufav
  2. 新建一个虚拟节点 node
  3. faufav 的父亲设为 node
  4. 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)]

例题BZOJ3732 Network

5.2 路径限制查询

问题:从某点出发,只能经过边权不超过某个值的边,最多能到达多少个点。

解法:在重构树上从该点向上倍增,找到最后一个点权不超过限制值的节点,该节点的子树大小即为答案。

例题P4768 [NOI2018] 归程

5.3 动态连通性问题

Kruskal重构树可以结合树剖、线段树等静态树结构,高效处理动态路径权值问题。

5.4 复杂图论问题

如 [IOI2018] 狼人问题,利用最小生成树和最大生成树分别建两棵重构树,解决路径上的多重约束问题。

六、总结

Kruskal重构树的核心思想可以概括为:

  1. 边权转点权:将Kruskal算法中加入的每条边转化为一个新节点
  2. 堆性质:建树方式决定了大根堆/小根堆的性质
  3. LCA性质:原图两点路径上的边权极值 = 重构树上两点LCA的点权

这一数据结构的优势在于,将图上的路径极值问题转化为了树上的LCA查询问题,从而可以用倍增、树剖等成熟的树上算法高效求解。

posted @ 2026-08-11 11:12  飞花阁  阅读(6)  评论(0)    收藏  举报
//雪花飘落效果