【图论】Kruskal 重构树

Kruskal重构树 (Kruskal Reconstruction Tree)

Kruskal重构树是一种在图论中用于解决特定问题的数据结构,它基于经典的Kruskal最小生成树算法构建。它并非直接用于求解最小生成树,而是通过重构原图的边,将边权信息“上移”到新生成的节点上,从而将路径上的边权最值问题转化为树上路径的节点最值问题,并能方便地处理连通性LCA(最近公共祖先)相关的问题。


一、核心思想

  1. 边权转点权:在原图中,边有权值。Kruskal重构树将这些边权值“提升”为新引入的内部节点的点权。
  2. 构建二叉树结构:对于原图中的每一条边 (u, v, w),我们创建一个新的节点 p,其点权为 w。然后将 p 作为 uv 在当前并查集中的“代表元”(根节点)的父节点。这样,p 就连接了包含 uv 的两个连通分量。
  3. 保持连通性信息:最终形成的是一棵满二叉树(每个非叶子节点都有两个子节点),叶子节点是原图的顶点,内部节点对应原图的边(按Kruskal顺序添加)。
  4. 性质
    • 最大生成树/最小生成树:如果按边权升序排序,则重构树对应最小生成树的连通过程;如果按边权降序排序,则对应最大生成树的连通过程。
    • 路径最值:在重构树上,任意两个叶子节点 uv 之间的简单路径上的最大点权(内部节点的权值),就等于原图中从 uv 的路径上边权的最大值(对于最大生成树版本)或最小值(对于最小生成树版本)。这得益于Kruskal算法的贪心性质。
    • LCA与连通性uv 的LCA节点的点权,就是连接 uv 所在连通分量时所用的那条边的权值。

二、应用场景

  1. 最小瓶颈路问题 (Minimum Bottleneck Path):求两点间路径上最大边权的最小值。使用边权升序构建的重构树,答案就是LCA的点权。
  2. 最大瓶颈路问题 (Maximum Bottleneck Path):求两点间路径上最小边权的最大值。使用边权降序构建的重构树,答案就是LCA的点权。
  3. 在线查询:预处理重构树和LCA后,可以在 O(log n) 时间内回答任意两点间的瓶颈值查询。
  4. 可持久化并查集模拟:重构树隐式地记录了并查集的合并历史。

三、构建步骤 (以最大生成树版本为例)

  1. 初始化
    • 将原图的 n 个顶点作为叶子节点,编号为 1n
    • 创建一个并查集,初始时每个顶点独立。
    • 将所有边按边权降序排序。
    • idx = n (下一个新节点的编号)。
  2. 遍历边
    • 对于每条边 (u, v, w),按排序后的顺序处理:
      • 在并查集中查找 uv 的根节点 fufv
      • 如果 fu != fv (即不在同一连通分量),则:
        • idx++,创建新节点 idx
        • 设置新节点的点权 val[idx] = w
        • fufv 的父节点都设置为 idx。即 parent[fu] = idx, parent[fv] = idx
        • 在并查集中合并 fufv
  3. 完成:处理完所有边后,得到一棵包含 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;
}

五、代码说明

  1. 数据结构
    • Edge: 存储边的信息。
    • edges: 存储所有边。
    • tree: 重构树的邻接表。
    • val: 重构树节点的点权。
    • dsu_parent: 并查集数组。
    • parent: 重构树的父节点指针(用于找根)。
    • depth, fa: 用于LCA倍增算法。
  2. 函数
    • 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可以高效解决一类瓶颈路径查询问题。理解其构建过程和性质是掌握它的关键。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值