C++并查集

C++ 并查集详解:从理论到实践

在计算机科学中,并查集(Disjoint Set Union,DSU) 是一种高效处理集合合并与查询问题的数据结构。它特别适合解决连通性问题,如网络连接状态、图的连通分量、最小生成树等。本文将深入解析 C++ 中的并查集实现、优化技术及典型应用。

1. 并查集的基本原理

并查集的核心操作有两个:

  • 查找(Find):确定元素所属的集合(即找到根节点)。
  • 合并(Union):将两个集合合并为一个。

其数据结构通常用数组表示,每个元素存储其父节点的索引。初始时,每个元素自成一个集合,根节点指向自己。

2. C++ 实现:从朴素到优化
2.1 朴素实现

最基础的并查集实现包含初始化、查找和合并三个方法:

class UnionFind {
private:
    vector<int> parent;  // 存储每个节点的父节点

public:
    // 初始化:每个节点的父节点是自己
    UnionFind(int n) {
        parent.resize(n);
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    // 查找根节点
    int find(int x) {
        while (parent[x] != x) {
            x = parent[x];
        }
        return x;
    }

    // 合并两个集合
    void unionSets(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX != rootY) {
            parent[rootY] = rootX;
        }
    }
};

 

问题:朴素实现的查找操作时间复杂度为 O (h),其中 h 是树的高度。当树退化为链表时,性能会显著下降。

2.2 路径压缩优化

路径压缩是指在查找过程中,直接将节点连接到根节点,从而减少后续查询的时间:

class UnionFind {
private:
    vector<int> parent;

public:
    UnionFind(int n) {
        parent.resize(n);
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    // 路径压缩:递归将节点直接连接到根节点
    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);  // 递归查找并压缩路径
        }
        return parent[x];
    }

    void unionSets(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX != rootY) {
            parent[rootY] = rootX;
        }
    }
};

优化效果:单次查找的平均时间复杂度接近 O (1)。
2.3 按秩合并(Union by Rank)
按秩合并是指在合并时,将较小的树连接到较大的树的根节点,避免树的高度过快增长:

class UnionFind {
private:
    vector<int> parent;
    vector<int> rank;  // 存储树的高度或节点数

public:
    UnionFind(int n) {
        parent.resize(n);
        rank.resize(n, 1);  // 初始秩为1
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    int find(int x) {
        if (parent[x] != x) {
            parent[x] = find(parent[x]);
        }
        return parent[x];
    }

    void unionSets(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX == rootY) return;

        // 将秩较小的树连接到秩较大的树
        if (rank[rootX] < rank[rootY]) {
            parent[rootX] = rootY;
        } else if (rank[rootX] > rank[rootY]) {
            parent[rootY] = rootX;
        } else {
            // 秩相同时,任选一个作为根,并增加其秩
            parent[rootY] = rootX;
            rank[rootX]++;
        }
    }
};

优化效果:结合路径压缩和按秩合并后,每个操作的平均时间复杂度为 O (α(n)),其中 α 是阿克曼函数的反函数,几乎可视为常数。
3. 高级应用:带权并查集
在某些场景中,我们需要维护节点间的关系(如距离、方向等),此时可使用带权并查集:

class WeightedUnionFind {
private:
    vector<int> parent;
    vector<int> weight;  // 存储节点到父节点的权值

public:
    WeightedUnionFind(int n) {
        parent.resize(n);
        weight.resize(n, 0);  // 初始权值为0
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
    }

    // 带权路径压缩
    int find(int x) {
        if (parent[x] != x) {
            int origParent = parent[x];
            parent[x] = find(parent[x]);
            weight[x] += weight[origParent];  // 更新权值
        }
        return parent[x];
    }

    // 带权合并
    void unionSets(int x, int y, int w) {
        int rootX = find(x);
        int rootY = find(y);
        if (rootX == rootY) return;

        parent[rootX] = rootY;
        weight[rootX] = weight[y] + w - weight[x];  // 更新权值
    }
};

应用场景:
食物链问题(判断物种间的捕食关系)
区间求和(维护区间关系)
4. 典型问题与解决方案
4.1 连通分量计数
统计无向图中的连通分量个数:

int countConnectedComponents(int n, vector<vector<int>>& edges) {
    UnionFind uf(n);
    for (auto& edge : edges) {
        uf.unionSets(edge[0], edge[1]);
    }

    // 计算根节点个数
    unordered_set<int> roots;
    for (int i = 0; i < n; i++) {
        roots.insert(uf.find(i));
    }
    return roots.size();
}

4.2 Kruskal 最小生成树算法
并查集可高效判断加入一条边是否会形成环:

int kruskal(int n, vector<vector<int>>& edges) {
    // 按边权排序
    sort(edges.begin(), edges.end(), [](const vector<int>& a, const vector<int>& b) {
        return a[2] < b[2];
    });

    UnionFind uf(n);
    int mstWeight = 0;
    int edgeCount = 0;

    for (auto& edge : edges) {
        int u = edge[0], v = edge[1], w = edge[2];
        if (uf.find(u) != uf.find(v)) {
            uf.unionSets(u, v);
            mstWeight += w;
            edgeCount++;
            if (edgeCount == n - 1) break;  // MST有n-1条边
        }
    }

    return mstWeight;
}

4.3 岛屿数量问题
计算二维网格中的连通岛屿数量:

int numIslands(vector<vector<char>>& grid) {
    if (grid.empty()) return 0;
    int rows = grid.size(), cols = grid[0].size();
    UnionFind uf(rows * cols);

    // 方向数组:右、下
    vector<pair<int, int>> directions = {{0, 1}, {1, 0}};

    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (grid[i][j] == '0') continue;

            // 将二维坐标映射为一维
            int id = i * cols + j;

            // 检查右侧和下方的相邻节点
            for (auto& dir : directions) {
                int ni = i + dir.first, nj = j + dir.second;
                if (ni < rows && nj < cols && grid[ni][nj] == '1') {
                    int neighborId = ni * cols + nj;
                    uf.unionSets(id, neighborId);
                }
            }
        }
    }

    // 统计根节点个数(排除水域)
    unordered_set<int> roots;
    for (int i = 0; i < rows; i++) {
        for (int j = 0; j < cols; j++) {
            if (grid[i][j] == '1') {
                roots.insert(uf.find(i * cols + j));
            }
        }
    }
    return roots.size();
}

5. 性能分析与最佳实践
时间复杂度:结合路径压缩和按秩合并后,每个操作的平均时间复杂度为 O (α(n)),其中 α(n) ≤ 4(对于实际应用中的 n)。
空间复杂度:O (n),用于存储父节点和秩。
最佳实践:
优先使用路径压缩和按秩合并的组合优化。
对于带权并查集,需特别注意权值的传递和更新逻辑。
在实际问题中,灵活设计节点的映射关系(如二维坐标转一维)。
6. 总结与扩展
并查集是解决连通性问题的高效工具,其核心在于路径压缩和按秩合并的优化。在 C++ 中,通过模板和类封装可轻松实现一个通用的并查集框架。
扩展思考:
如何实现可持久化并查集(支持查询历史版本)?
并查集与其他数据结构(如线段树、树状数组)的结合应用?
在分布式系统中,如何设计并查集算法?
通过深入理解并查集的原理和应用,我们可以高效解决诸如最小生成树、图的连通性、社交网络分析等实际问题。

<think>好的,用户想要查找关于C++实现并查集的数据结构的资料或教程,还希望有示例代码。我需要先回忆并查集的基本概念和常见实现方式。并查集主要用于处理不相交集合的合并与查询问题,常见操作有查找根节点和合并两个集合。 用户提供的引用中有提到并查集的实现思路,比如引用[2]提到增加一个数组存储节点到父节点的权值,还有路径压缩和按秩合并的优化方法。引用[3]则提到使用前需要明确合并规则和查询需求,初始化时要确定集合大小,合并前先查询是否属于不同集合。这些信息可以作为参考。 首先,我应该解释并查集的基本结构,包括父节点数组和秩数组。然后分步骤讲解初始化、查找和合并操作。查找操作需要路径压缩,合并操作需要按秩合并,这两个优化能提高效率。接着给出一个基础的C++实现示例,包括类的定义和相关方法。 然后,考虑到用户可能需要更高级的功能,比如带权并查集,可以基于引用[2]提到的增加权值数组,解释如何扩展实现。这部分需要说明权值的含义和在查找、合并时的处理方式,确保权值正确更新。 最后,生成相关问题,帮助用户进一步学习,比如优化方法、时间复杂度、应用场景等。这些需要结合引用中的信息,确保问题相关且有深度。 需要注意的是,代码示例要符合C++语法,正确使用路径压缩和按秩合并,注释清晰。同时,引用用户提供的资料时要正确标注引用标记,比如路径压缩的部分引用[2],注意事项引用[3]。 检查是否有遗漏的部分,比如并查集的应用场景,或者常见错误,比如忘记初始化父数组等。确保回答结构清晰,步骤明确,代码正确,示例易懂。</think>以下是为您整理的C++并查集数据结构实现详解: ### 一、并查集基础实现 #### 1.1 数据结构定义 基础并查集包含两个核心数组: - $parent[]$ 存储每个元素的父节点 - $rank[]$ 存储集合的秩(用于优化合并操作) ```cpp class UnionFind { private: vector<int> parent; vector<int> rank; public: UnionFind(int size) { parent.resize(size); rank.resize(size, 0); // 初始化每个元素的父节点为自身 for(int i = 0; i < size; ++i) parent[i] = i; } }; ``` #### 1.2 核心操作实现 **查找操作(带路径压缩)**: ```cpp int find(int x) { if (parent[x] != x) parent[x] = find(parent[x]); // 路径压缩优化[^2] return parent[x]; } ``` **合并操作(按秩合并)**: ```cpp void unite(int x, int y) { int rootX = find(x); int rootY = find(y); if (rootX != rootY) { // 属于不同集合时才合并[^3] if (rank[rootX] > rank[rootY]) parent[rootY] = rootX; else { parent[rootX] = rootY; if (rank[rootX] == rank[rootY]) rank[rootY]++; } } } ``` ### 二、带权并查集扩展 基于引用[2]的权值存储思路,增加权值数组: ```cpp class WeightedUnionFind { private: vector<int> parent; vector<int> weight; // 存储到父节点的权值 public: WeightedUnionFind(int size) { parent.resize(size); weight.resize(size, 0); for(int i=0; i<size; ++i) parent[i] = i; } int find(int x) { if(parent[x] != x) { int orig_parent = parent[x]; parent[x] = find(parent[x]); weight[x] += weight[orig_parent]; // 权值更新 } return parent[x]; } void unite(int x, int y, int value) { int rootX = find(x); int rootY = find(y); if(rootX != rootY) { parent[rootX] = rootY; weight[rootX] = value - weight[x] + weight[y]; } } }; ``` ### 三、应用场景 1. 图的连通性判断 2. 最小生成树算法(Kruskal算法) 3. 动态连接问题 4. 社交网络好友关系维护
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值