union-find算法研究

union-find算法研究

union-find 是对图的一些算法的讨论。在我们的讨论中,我们使用整数标识符来作为图的一个触点。

术语

  • 动态连通性

    如果 q 和 p 是相连的,那么:

    • 自反性:p 和 p 也是相连的

    • 对称性:p 和 q 也是相连的

    • 传递性:如果 p 和 q 是相连的,而 q 和 r 是相连的,那么 p 和 r 是相连的。

  • 数学集合

    将对象称为触点,将整数对称为连接,将等价类称为连通分量或是简称分量。

union-find算法的API

public class UF

返回类型方法名描述
UF(int N)以整数标识(0到N-1)初始化N个触点
voidunion(int p, int q)在 p 和 q 之前添加一条连接
intfint(int p)p 所在的分量的标识符
booleanconnected(int p, int q)如果 p 和 q 存在于同一个分量中则返回 true
intcount()分量的个数

union-find算法实现

一开始我们有 N 个分量,每个触点都构成了一个只含有自己的分量,因此我们将 id[i] 的值初始化为 i,其中 i 在 0 到 N-1之间。对于触点 i 我们将 find() 方法用来判定它所在的分量所需的信息保存在 id[i] 中。connected() 方法的实现只用一条语句 find(p)==find(q),它返回一个布尔值,我们所有的方法的实现都会用到connected()方法

我们维护了两个实例变量,一个是连通分量的个数,一个是数组 id[ ]。

package com.li.union_find;

/*并查集undion-find的简单实现*/

public class Union_find {

    private int[] id;    //分量id(以触点作为索引)
    private int count;   //分量数量

    /*初始化分量id数组*/
    public Union_find(int N) {
        count=N;
        id=new int[N];
        for (int i=0; i<N; i++)
            id[i]=i;
    }

    public int count() {
        return count;
    }

    public int find(int p) {
        //详细实现
    }

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

    public void union(int p, int q) {
        //详细实现
    }

}

quick-find算法

一种方法是保证当且仅当 id[p] 等于 id[q],p和q是连通的.即同一连通分量中的所有触点 id[ ]的值全部相同,这意味着connected()只需判断id[p]和id[q]的值是否相等即可.

调用union()将p和q归并到相同的分量中,如果 id[p] == id[q],则不需要进行任何改变,否则遍历整个数组id,将所有和 id[p] 相等的元素变成 id[q],这样我们就将 p 所在的整个分量加入了 q 所在的连通分量中,当然也可以将所有和id[q]相等的元素变成id[p]。

public int find(int p) {
    return id[p];
}

/*将p和q归并到相同的分量中*/
public void union(int p, int q) {
    //如果已经在同一分量中,不采取行动
    if(id[p]==id[q])
        return;

    int pID=find(p);
    int qID=find(q);

    for(int i=0; i<id.length; i++) {
        if(id[i]==pID)
            id[i]=qID;
    }
    count--;
}
quick-union算法分析

find() 操作的速度显然很快,因为它只需要访问一次数组。但是此算法的 union() 操作一般无法处理大型的问题,因为对于每一对输入 union() 都需要扫描整个 id[] 数组。

quick-union算法

这次我们讨论的重心就是提高 union() 方法的速度,可以说,它和 quick-find 算法时互补的。我们知道,链表的连接以及添加是很快的,我们可以基于这种结构来重新构造相同的数据结构。

如下:每个触点所对应的 id[] 元素都是同一个分量中的另一个触点的名称(也可能是它自己)——我们将这种联系称为链接。在实现 find() 方法时,我们从给定的触点开始,由它的链接得到另一个触点,再由这个触点的链接到达第三个触点,如此继续跟着链接直到到达一个跟触点,即链接指向自己的触点。

package com.li.union_find;

public class Quick_union {
    private int[] id;    //分量id(以触点作为索引)
    private int count;   //分量数量

    /*初始化分量id数组*/
    public Quick_union(int N) {
        count=N;
        id=new int[N];
        for (int i=0; i<N; i++)
            id[i]=i;
    }

    public int count() {
        return count;
    }

    public int find(int p) {
        //找出分量的名称
        while(p!=id[p])
            p=id[p];
        return p;
    }

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

    /*将p和q归并到相同的分量中*/
    public void union(int p, int q) {

        int pRoot=find(p);
        int qRoot=find(q);

        if(qRoot==pRoot)
            return;

        id[pRoot]=qRoot;
        count--;
    }
}
森林的表示

我们如果用节点(带标签的圆圈)表示触点,用从一个节点到另一个节点的箭头表示链接,由此得到数据结构的图像表示使我们理解算法的操作变得相对容易。我们得到的结构是树——从技术上讲,id[ ] 数组用父链接的形式表示了一片森林。

定义

一棵树的大小是它的节点的数量。树中的一个节点的深度是它到根节点的路径上的链接数。数的高度是它的所有节点中的最大深度。

命题

quick-union 算法中的 find() 方法访问数组的次数为 1 加上给定触点所对应的节点中的深度的两倍。union() 和 connected()访问数组的次数为两次 find() 操作(如果 union() 中给定的两个触点分别存在于不同的树中则还需要加 1)。

加权 quick-union算法

与其在 union() 中随意将一棵树连接到另一棵树,我们现在回记录一棵树的大小并总是将较小的树连接到较大的树上。这项改动需要添加一个数组和一些代码来记录树中的节点数,它能够大大改进算法的效率,我们将它称为加权 quick-union 算法。

package com.li.union_find;

public class WeightedQuickUnion {
    private int[] id;    //分量id(以触点作为索引)
    private int count;   //分量数量
    private int[] sz;    //各个根节点所对应的分量的大小

    public WeightedQuickUnion(int N) {
        count=N;
        id=new int[N];
        for(int i=0; i<N; i++)
            id[i]=i;
        sz=new int[N];
        for(int i=0; i<N; i++)
            sz[i]=1;
    }

    public int count() {
        return count;
    }

    public int find(int p) {
        //找出分量的名称
        while(p!=id[p])
            p=id[p];
        return p;
    }

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

    public void union(int p, int q) {
        int pRoot=find(p);
        int qRoot=find(q);

        if(pRoot==qRoot)
            return;

        /*将小树的根节点连接到大树的根节点*/
        if(sz[pRoot]<sz[qRoot]) {
            id[pRoot]=qRoot;
            sz[qRoot]+=sz[pRoot];
        }
        else {
            id[qRoot]=pRoot;
            sz[pRoot]+=sz[qRoot];
        }
        count--;
    }
}
算法分析

把小树连接到较大的树上,保证了树的平均深度较小,从而在进行 find() 操作时运行时间更少。

命题:对于 N 个触点,加权 quick-union 算法构造的森林中的任意节点的深度最多为 lgN l g N

证明:归纳法证明一个更强的命题,即森林中大小为 K 的树的高度最多为 lgK l g K

推论:对应加权 quick-union 算法和 N 个触点,在最坏的情况下,find()、connected() 和 union() 的成本的增长数量级为 lgN l g N

各种 union-find 算法的性能特点

在最坏的情况下

最优算法

理想情况下,我们希望每个节点都直接链接到它的根节点上,但我们又不希望像 union-find 算法那样通过修改大量链接做到这一点,这时可以通过在检查节点的同时把它直接链接到根节点上面去。

要实现路径压缩,只需要为find()添加一个循环,将在路径上遇到的节点全部链接到根节点。

路径压缩的加权quick-union算法是最优的算法,但并非所有操作都能在常数时间内完成。

package com.li.union_find;

import com.li.stdlib.StdIn;
import com.li.stdlib.StdOut;

/*使用路径压缩的加权 quick-union 算法*/

public class WeightedQuickUnionPathCompressionUF {
    private int[] parent; // parent[i] = parent of i
    private int[] size; // size[i] = number of sites in tree rooted at i
    private int count; // number of components

    public WeightedQuickUnionPathCompressionUF(int N) {
        count = N;
        parent = new int[N];
        size = new int[N];
        for (int i = 0; i < N; i++) {
            parent[i] = i;
            size[i] = 1;
        }
    }

    public int count() {
        return count;
    }

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

    public int find(int p) {
        validate(p);
        int root = p;

        /* find the root */
        while (root != parent[root])
            root = parent[root];

        /* path compression */
        while (p != root) {
            int newp = parent[p];
            parent[p] = root;
            p = newp;
        }
        return root;
    }

    // validate that p is a valid index
    private void validate(int p) {
        int N = parent.length;
        if (p < 0 || p >= N) {
            throw new IllegalArgumentException("index " + p + " is not between 0 and " + (N - 1));
        }
    }

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

        // make smaller root point to larger one
        if (size[rootP] < size[rootQ]) {
            parent[rootP] = rootQ;
            size[rootQ] += size[rootP];
        } else {
            parent[rootQ] = rootP;
            size[rootP] += size[rootQ];
        }
        count--;
    }

    public static void main(String[] args) {
        int n = 10;
        WeightedQuickUnionPathCompressionUF uf = new WeightedQuickUnionPathCompressionUF(n);
        while (!StdIn.isEmpty()) {
            int p = StdIn.readInt();
            int q = StdIn.readInt();
            if (uf.connected(p, q))
                continue;
            uf.union(p, q);
            StdOut.println(p + " " + q);
        }
        StdOut.println(uf.count() + " components");
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值