Union_Find

并查集算法

->解决动态连通性问题

供自己使用的学习笔记,原文链接:https://cloud.tencent.com/developer/article/1880833

主要API:

class UF {
    /* 将 p 和 q 连接 */
    public void union(int p, int q);
    /* 判断 p 和 q 是否连通 */
    public boolean connected(int p, int q);
    /* 返回图中有多少个连通分量 */
    public int count();
}

我们使用森林(若干棵树)来表示图的动态连通性,用数组来具体实现这个森林。

具体实现:

public void union(int p, int q) 
{
    int rootP = find(p);
    int rootQ = find(q);
    if (rootP == rootQ)
        return;
    // 将两棵树合并为一棵
    parent[rootP] = rootQ;			/*一个树的树根接到另一个树上*/
    // parent[rootQ] = rootP 也一样
    count--; // 两个分量合二为一
}

/* 返回某个节点 x 的根节点 */
private int find(int x) 
{
    // 根节点的 parent[x] == x
    while (parent[x] != x)			/*不断寻找父节点直到根父节点*/
        x = parent[x];
    return x;
}

/* 返回当前的连通分量个数 */
public int count() 
{ 
    return count;
}

/*判断是否连通,即判断祖先是否相同即可*/
public boolean connected(int p, int q) 
{
    int rootP = find(p);
    int rootQ = find(q);
    return rootP == rootQ;
}

时间复杂度如何呢?

我们发现union函数和connected函数的时间复杂度取决于find函数,而find的时间复杂度取决于树的高度,平均情况即O(logN),最坏情况变为单支树,变成了O(N).

->那么如何避免树的不平衡呢?我们引出下面的平衡性优化。

我们要知道哪种情况下可能出现不平衡现象,关键在于union过程:

我们一开始就是简单粗暴的把p所在的树接到q所在的树的根节点下面,那么这里就可能出现「头重脚轻」的不平衡状况,比如下面这种局面:

preload

长此以往,树可能生长得很不平衡。我们其实是希望,小一些的树接到大一些的树下面,这样就能避免头重脚轻,更平衡一些。解决方法是额外使用一个size数组,记录每棵树包含的节点数,我们不妨称为「重量」:

class UF {
    private int count;
    private int[] parent;
    // 新增一个数组记录树的“重量”
    private int[] size;

    public UF(int n) {
        this.count = n;
        parent = new int[n];
        // 最初每棵树只有一个节点
        // 重量应该初始化 1
        size = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }
    /* 其他函数 */
}

这样,通过比较树的重量,就可以保证树的生长相对平衡,树的高度大致在logN这个数量级,极大提升执行效率。

此时,find,union,connected的时间复杂度都下降为 O(logN),即便数据规模上亿,所需时间也非常少。

再优化->路径压缩,使树高保持为常数:

要做到这一点,非常简单,只需要在find中加一行代码:

private int find(int x) {
    while (parent[x] != x) {
        // 进行路径压缩
        parent[x] = parent[parent[x]];
        x = parent[x];
    }
    return x;
}

调用find函数每次向树根遍历的同时,顺手将树高缩短了,最终所有树高都不会超过 3(union的时候树高可能达到 3)。

最后总结:完整代码

class UF {
    // 连通分量个数
    private int count;
    // 存储一棵树
    private int[] parent;
    // 记录树的“重量”
    private int[] size;

    public UF(int n) {
        this.count = n;
        parent = new int[n];
        size = new int[n];
        for (int i = 0; i < n; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    public void union(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        if (rootP == rootQ)
            return;

        // 小树接到大树下面,较平衡
        if (size[rootP] > size[rootQ]) {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        } else {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        }
        count--;
    }

    public boolean connected(int p, int q) {
        int rootP = find(p);
        int rootQ = find(q);
        return rootP == rootQ;
    }

    private int find(int x) {
        while (parent[x] != x) {
            // 进行路径压缩
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }
}


 {
        while (parent[x] != x) {
            // 进行路径压缩
            parent[x] = parent[parent[x]];
            x = parent[x];
        }
        return x;
    }
}
### Kruskal 最小生成树算法Union-Find 的实现 Kruskal 算法是一种用于寻找加权无向图中的最小生成树的经典贪心算法。其核心思想是按照权重从小到大依次选取边,同时利用 **Union-Find** 数据结构来检测并避免形成环路。 #### 1. 基本概念 - 图的生成树是一个连通子图,包含原图的所有顶点以及恰好 $ n-1 $ 条边[^4]。 - 如果生成树中每条边的权重之和是最小的,则称之为最小生成树 (Minimum Spanning Tree, MST)[^3]。 #### 2. Kruskal 算法的核心步骤 以下是 Kruskal 算法的主要流程: - 将所有的边按权重升序排列。 - 初始化一个空的结果集(存储最终的最小生成树)。 - 遍历排序后的边列表,对于当前边 $(u, v)$,判断顶点 $ u $ 和 $ v $ 是否属于同一个连通分量: - 若不属于同一连通分量,则将此边加入结果集中,并通过 **Union** 操作将两个顶点所在的集合合并。 - 否则跳过这条边,因为将其加入会形成环路。 - 当结果集中的边数达到 $ n-1 $ 或者遍历完成时停止操作。 这一过程中,**Union-Find** 是用来快速查询和更新连通性的工具。 --- #### 3. Union-Find 数据结构的作用 Union-Find 又被称为并查集,主要用于动态维护一组不相交集合的数据结构。它的功能包括: - **Find**: 查找某个节点所属的集合编号。 - **Union**: 连接两个不同的集合,使它们成为一个新的集合。 为了提高效率,通常采用路径压缩和按秩合并两种优化策略。 ##### 路径压缩 在执行 `find` 操作时,将沿途经过的所有节点直接连接到根节点上,从而减少后续查找的时间开销。 ##### 按秩合并 在执行 `union` 操作时,总是将较小的树挂接到较大的树下,这样可以保持树的高度较低,进一步提升性能。 这些优化使得单次操作的均摊时间复杂度接近于常数级别。 --- #### 4. Java 示例代码 下面展示了一个完整的基于 Kruskal 算法的最小生成树实现: ```java import java.util.*; class Edge implements Comparable<Edge> { int src; int dest; int weight; public Edge(int src, int dest, int weight) { this.src = src; this.dest = dest; this.weight = weight; } @Override public int compareTo(Edge other) { return Integer.compare(this.weight, other.weight); } } public class KruskalAlgorithm { private static int findParent(int[] parent, int node) { if (parent[node] != node) { parent[node] = findParent(parent, parent[node]); // Path compression } return parent[node]; } private static void unionSets(int[] parent, int[] rank, int xRoot, int yRoot) { if (rank[xRoot] < rank[yRoot]) { parent[xRoot] = yRoot; // Attach smaller tree under larger one } else if (rank[xRoot] > rank[yRoot]) { parent[yRoot] = xRoot; } else { // If ranks are the same parent[yRoot] = xRoot; rank[xRoot]++; } } public static List<Edge> kruskalMST(List<Edge> edges, int V) { Collections.sort(edges); // Sort all edges by their weights List<Edge> result = new ArrayList<>(); int[] parent = new int[V]; int[] rank = new int[V]; for (int i = 0; i < V; ++i) { parent[i] = i; // Initialize each vertex as its own parent rank[i] = 0; } int e = 0; // Number of edges added to the MST int i = 0; // Index variable for sorted edges while (e < V - 1 && i < edges.size()) { Edge nextEdge = edges.get(i++); int xRoot = findParent(parent, nextEdge.src); int yRoot = findParent(parent, nextEdge.dest); if (xRoot != yRoot) { // No cycle formed result.add(nextEdge); unionSets(parent, rank, xRoot, yRoot); e++; } } return result; } public static void main(String[] args) { int V = 4; // Number of vertices in graph List<Edge> edges = Arrays.asList( new Edge(0, 1, 10), new Edge(0, 2, 6), new Edge(0, 3, 5), new Edge(1, 3, 15), new Edge(2, 3, 4) ); List<Edge> mstEdges = kruskalMST(edges, V); System.out.println("Edges in Minimum Spanning Tree:"); for (Edge edge : mstEdges) { System.out.printf("%d -- %d == %d\n", edge.src, edge.dest, edge.weight); } } } ``` 上述程序实现了 Kruskal 算法的关键逻辑,其中包括了对边的排序、使用 Union-Find 处理连通性和循环检测等功能[^2]。 --- #### 5. 总结 Kruskal 算法依赖于边的有序性以及高效的集合管理机制。借助 **Union-Find** 数据结构,可以在几乎线性时间内找到最优解。这种组合不仅简化了问题处理难度,还显著提升了计算效率。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值