高效解决图连通性 – 并查集 Union Find

并查集的用处 – 分组和查询分组

并查集可以快速地分组和查询分组,时间复杂度近似常数,通常用于解决图的 连通性 问题。分组时使用 并操作 将元素划为一组或加入同一个连通分量,查询组别或所属连通子图时 使用 查操作 返回组中

单节点 – 单节点连通性

通过分组将联通分量分为同一组
检测大型电路中两个点是否连通
检测网络中两个节点是否可达到
检测社交网络中两人是否有关系

多节点 – 多节点连通性

在多节点端外额外添加一个节点作为根节点
将多节点与根节点分为一组
遍历图将联通分量分为同一组
仅需检测两端额外添加的根节点是否同一组别

并查集的表示

abstract class UF {
    protected int id[];                         // 组标识
    protected int sz[];                         // 元素数
    protected int count;                        // 总组数
    public abstract int find(int i);            // 查操作
    public abstract void union(int p, int p);   // 并操作
    public boolean connected(int p, int q) {    // 判连接
        return find(p) == find(q);
    }
    public UF(int N){                   // 初始化为N个组别
        count = N;
        id = new int[N];
        sz = new int[N];
        for(int i = 0; i < N; i++) {
            id[i] = i;
            sz[i] = 1;
        }
    }
}

并查集的实现

快速查找 Quick Find – 新加节点id作为组标识

表示法 每个元素各自存储所属的组id,同一分组的组id相同
并操作 把容纳组节点存储的组标识一一修改为新加节点的id
查操作 读取元素存储的组标识id
复杂度 并操作\scriptsize O(n),查操作\scriptsize O(1)

class QuickFindUF extends UF {
    @Override
    public int find(int i) {
        return id[i];
    }
    @Override
    public void union(int p, int q){
        int pId = find(p);
        int qId = find(q);
        if(pId == qId) return;
        for(int i = 0; i < id.length; i++)
            if(id[i] == pId) id[i] = qId;
        count--;
    }
}

快速合并 Quick Union – 根节点id作为组标识

表示法 仅用根节点id唯一标识组别,子节点链接根节点来读取标识
并操作 合并两节点实际是合并两个组,即将并入组根节点成为容纳组根节点的子树
查操作 根据链接查询至根节点,读取组标识id
复杂度 并查操作均为 \scriptsize O(height(tree))

class QuickUnionUF extends UF {
    @Override
    public int find(int i) {
        if(i == id[i]) return i;
        return find(id[i]);
    }
    @Override
    public void union(int p, int q){
        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot) return;
        id[qRoot] = pRoot;
        count--;
    }
}

带权快速合并 Weighted Quick Union – 小树成子树

表示法 维护树的容量字段,让小规模树成为大规模树的子节点,防止树过高而降低查询链接速度
并操作 改进快速合并,将小树根节点成为大树根节点的子节点,同时修改树的大小
查操作 同快速合并,链接查询至根节点
复杂度 并查操作均 \scriptsize O(\log n)

class WeightedQuickUnionUF extends UF {
    @Override
    public int find(int i) {
        if(i == id[i]) return i;
        return find(id[i]);
    }
    @Override
    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--;
    }
}

路径压缩快速合并 Path Compression Quick Union – 直接挂树根

表示法 快速合并的改进。查操作检索根节点同时,将所有被检索节点指向根节点,减小树高度
并操作 同快速合并
查操作 同快速合并,区别是向上检索节点时,将每个节点都指向根节点
复杂度 并查操作均接近 \scriptsize O(1)

class WeightedQuickUnionUF extends UF {
    @Override
    public int find(int i) {
        if(i == id[i]) return i;
        return id[i] = find(id[i]);
    }
    @Override
    public void union(int p, int q){
        int pRoot = find(p);
        int qRoot = find(q);
        if(pRoot == qRoot) return;
        id[qRoot] = pRoot;
        count--;
    }
}

路径压缩的带权快速合并 Weighted Path Compression Quick Union

结合带权和路径压缩两种优点,有效控制树的高度,从而并查操作性能逼近 \scriptsize O(1)

class WPCQuickUnion extends UF{
    @Override
    public int find(int i) {
        if(i == id[i]) return i;
        return id[i] = find(id[i]);
    }
    @Override
    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];
        }
    }
}

应用案例

POJ1703

/**
 * ---------------------------------------------------------------
 * - 实现思路
 *      己方和对方分为两组 -> 己方和对方的对方分为一组
 *      使用路径压缩的带权快速合并
 * - 实现细节
 *      初始每个节点以id为组标识符自成一组,额外内存op保存节点的对方
 *      依据对方节点的op查询己方节点分入己方组,op为0的节点还未分组
 *      每次合并分组时将未分组节点划入已分组节点
 * - 其他注意
 *      输入数据从1开始,数组开为N+1尺寸方便操作
 *      输入的两个数据均为己方时,造成数据矛盾
 * ---------------------------------------------------------------
 */
class UnionFind {
    private int[] id;
    private int[] op;
    private int[] sz;

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

    public int[] op(int i) {
        return op;
    }

    // union方法、find方法见路径压缩的带权快速合并
}

public static void main(String[] args) throws Exception {
    Scanner sc = new Scanner(System.in);
    UnionFind uf = new UnionFind(N);
    while (sc.hasNext()) {
        String flag = sc.next();
        int p = sc.nextInt();
        int q = sc.nextInt();
        switch (flag) {
            case "A":
                if (uf.find(p) == uf.find(q))
                    System.out.println("In the same gang.");
                else if (uf.find(p) == uf.find(uf.op[q]))
                    System.out.println("In different gangs.");
                else
                    System.out.println("Not sure yet.");
                break;

            case "D":
                if (uf.find(p) == uf.find(q))
                    throw new Exception("Ambiguous Data");
                if (uf.op[p] == 0) {
                    if (uf.op[q] == 0) {
                        uf.op[p] = q;
                        uf.op[q] = p;
                    }
                    else {
                        uf.op[p] = q;
                        uf.union(p, uf.op[q]);
                    }
                }
                else {
                    if (uf.op[q] == 0) {
                        uf.op[q] = p;
                        uf.union(q, uf.op[p]);
                    }
                    else {
                        uf.union(p, uf.op[q]);
                        uf.union(q, uf.op[p]);
                    }
                }
                break;

            default: throw new Exception("Non-Compliant Input");
        }
    }
}

初始状态如下所示

id [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
sz [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
op [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

新入节点1和节点2相互保存对方组别标识

> D 1 2
id [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
sz [1, 1, 1, 1, 1, 1, 1, 1, 1, 1]
op [0, 2, 1, 0, 0, 0, 0, 0, 0, 0]
       ↑  ↑

新入节点4依据节点1查询到己方节点2并分为一组

> D 1 4
id [0, 1, 4, 3, 4, 5, 6, 7, 8, 9]
sz [1, 1, 1, 1, 2, 1, 1, 1, 1, 1]
op [0, 2, 1, 0, 1, 0, 0, 0, 0, 0]
       ↑  ↑     ↑

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注

©2018-2024 Howell版权所有 备案号:冀ICP备19000576号