并查集

醉酒当歌 提交于 2021-01-29 17:26:51

算法:并查集

快速掌握

理解算法

  在计算机科学中,并查集是一种树型的数据结构,用于处理一些不交集(Disjoint Sets)的合并及查询问题。有一个联合-查找算法union-find algorithm)定义了两个用于此数据结构的操作:

  • Find确定元素属于哪一个子集。这个确定方法就是不断向上查找找到它的根节点,它可以被用来确定两个元素是否属于同一子集
  • Union将两个子集合并成同一个集合

  由于支持这两种操作,一个不相交集也常被称为联合-查找数据结构(union-find data structure)或合并-查找集合(merge-find set)。其他的重要方法,MakeSet,用于建立单元素集合。有了这些方法,许多经典的划分问题可以被解决。

  为了更加精确的定义这些方法,需要定义如何表示集合。一种常用的策略是为每个集合选定一个固定的元素,称为代表,以表示整个集合。接着,Find(x) 返回 x 所属集合的代表,而 Union 使用两个集合的代表作为参数

  

**说明:**左边是A,笔误!

  上图中简单演****示了并查集的两个操作,一个是FIND,一个UNION

并查集(树)

  并查集(树)是一种将一个集合以树形结构进行组合的数据结构,如上图所示。其中每一个节点保存着到它的父节点的引用

  在并查集树中,每个集合的代表即是集合的根节点

  • “查找”根据其父节点的引用向根行进直到到底树根
  • “联合”将两棵树合并到一起,这通过将一棵树的根连接到另一棵树的根

  实现这样操作的伪代码如下:

function MakeSet(x)
    x.parent := x
 
function Find(x)
    if x.parent == x
       return x
    else
       return Find(x.parent)
 
function Union(x, y)
    xRoot := Find(x)
    yRoot := Find(y)
    xRoot.parent := yRoot

  这是并查集树林的最基础的表示方法,这个方法不会比链表法好,这是因为创建的树可能会严重不平衡;然而,可以用两种办法优化。

优化方法一:按秩合并

  第一种方法,称为“按秩合并”,即总是将更小的树连接至更大的树上。因为影响运行时间的是树的深度,更小的树添加到更深的树的根上将不会增加秩除非它们的秩相同。在这个算法中,术语“秩”替代了“深度”,因为同时应用了路径压缩时(见下文)秩将不会与高度相同。单元素的树的秩定义为0,当两棵秩同为r的树联合时,它们的秩r+1。只使用这个方法将使最坏的运行时间提高至每个MakeSet、Union或Find操作、

优化后的MakeSetUnion伪代码:

function MakeSet(x)
    x.parent := x
    x.rank   := 0
 
function Union(x, y)
    xRoot := Find(x)
    yRoot := Find(y)
    if xRoot == yRoot
        return
 
    // x和y不在同一个集合,合并它们。
    if xRoot.rank < yRoot.rank
        xRoot.parent := yRoot
    else if xRoot.rank > yRoot.rank
        yRoot.parent := xRoot
    else
        yRoot.parent := xRoot
        xRoot.rank := xRoot.rank + 1

优化方法二:路径压缩 

  第二个优化,称为“路径压缩”,是一种在执行“查找”时扁平化树结构的方法。关键在于在路径上的每个节点都可以直接连接到根上;他们都有同样的表示方法。为了达到这样的效果,Find递归地经过树,改变每一个节点的引用到根节点。得到的树将更加扁平,为以后直接或者间接引用节点的操作加速。

  

这儿是Find

function Find(x)
    if x.parent != x
       x.parent := Find(x.parent)
    return x.parent

  这两种方法的优势互补,同时使用二者的程序每个操作的平均时间仅为!的反函数,其中是急速增加的阿克曼函数。因为是其的反函数,故十分巨大时还是小于5。因此,平均运行时间是一个极小的常数。

  实际上,这是渐近最优算法:Fredman和Saks在1989年解释了的平均时间内可以获得任何并查集。

并查集算法-Java实现

package leetcode;


import java.util.Arrays;

public class UnionFindSet {
    private int[] parents_;//父级节点
    private int[] ranks_;//秩

    public UnionFindSet(int n) {
        ranks_ = new int[n];
        Arrays.fill(this.ranks_, 1);
        parents_ = new int[n];
        for (int i = 0; i < n; i++) {
            parents_[i] = i;
        }
    }

    public boolean Union(int u, int v) {
        int pu = Find(u);
        int pv = Find(v);
        if (pu == pv)
            return false;
        if (ranks_[pv] > ranks_[pu])
            parents_[pu] = pv;
        else if (ranks_[pu] > ranks_[pv])
            parents_[pv] = pu;
        else {
            parents_[pv] = pu;
            ranks_[pu] += 1;
        }
        return true;
    }

    public int Find(int u) {
        while (parents_[u] != u) {
            parents_[u] = parents_[parents_[u]];
            u = parents_[u];
        }
        return u;
    }

}

主要操作

合并两个不相交集合

  操作很简单:先设置一个数组(阵列)Father[x],表示x的“父亲”的编号。 那么,合并两个不相交集合的方法就是,找到其中一个集合最父亲的父亲(也就是最久远的祖先),将另外一个集合的最久远的祖先的父亲指向它

void Union(int x,int y)
{
    fx = getfather(x);
    fy = getfather(y);
    if(fy!=fx)
       father[fx]=fy;
}

判断两个元素是否属于同一集合

  仍然使用上面的数组。则本操作即可转换为寻找两个元素的最久远祖先是否相同寻找祖先可以采用递归实现,见后面的路径压缩算法。

bool same(int x,int y)
{
   return getfather(x)==getfather(y);
}
/*返回true 表示相同根结点,返回false不相同*/

测试

//连接所有点的最小费用 https://leetcode-cn.com/problems/min-cost-to-connect-all-points/
class Solution {
    public static void main(String[] args) {
//        points = [[0,0],[2,2],[3,10],[5,2],[7,0]]
//        输出:20
        int[][] a = new int[][]{{0, 0}, {2, 2}, {3, 10}, {5, 2}, {7, 0}};
        int ints = new Solution().minCostConnectPoints(a);
        System.out.println(ints);
    }

    public int minCostConnectPoints(int[][] points) {
        int n = points.length; // 记录节点个数
        UnionFindSet dsu = new UnionFindSet(n);
        List<Edge> edges = new ArrayList<Edge>();
        for (int i = 0; i < n; i++) {
            for (int j = i + 1; j < n; j++) {
                edges.add(new Edge(dist(points, i, j), i, j));
            }
        }
        //算出所有组合边及权重edges.size edge.len
        System.out.println("加权重后:" + edges);
        Collections.sort(edges, new Comparator<Edge>() {
            public int compare(Edge edge1, Edge edge2) {
                return edge1.len - edge2.len;
            }
        });
        //根据权重排序
        System.out.println("权重按大小排序后:" + edges);
        int ret = 0, num = 1;// 记录每个连通分量的节点个数
        for (Edge edge : edges) {
            int len = edge.len, x = edge.x, y = edge.y;
            if (dsu.Union(x, y)) {
                ret += len;
                num++;
                if (num == n) {
                    break;
                }
            }
        }
        return ret;
    }

    //加权重
    public int dist(int[][] points, int x, int y) {
        return Math.abs(points[x][0] - points[y][0]) + Math.abs(points[x][1] - points[y][1]);
    }
}

class Edge {
    int len, x, y;

    public Edge(int len, int x, int y) {
        this.len = len;// 长度
        this.x = x; // 顶点1
        this.y = y; // 顶点2
    }
}

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!