想要精通算法和SQL的成长之路 - 并查集的运用和案例(省份数量)

前言

想要精通算法和SQL的成长之路 - 系列导航

一. 并查集的使用和模板

先说一下并查集的相关知识点:

  • 含义:并查集,用于维护一组不相交的集合,支持合并两个集合和查询某个元素所属的集合。
  • 用途:解决图论、连通性问题和动态连通性等问题。

通俗一点,可以使用并查集的算法题目有哪些特征?

  • 需要将n个不同的元素划分为不相交的集合。
  • 开始的时候,每个元素自行成为一个集合,然后需要根据一定的顺序进行 合并
  • 同时还需要 查询 某个元素是否属于哪个集合。

因此并查集的基本操作可以包含两个:

  • 合并:将两个不相交的集合合并成一个集合。(将其中一个集合的根节点连接到另一个集合的根节点上)
  • 查找:根据某个元素,寻找到它所在集合的根节点。

1.1 初始化

首先我们考虑下,并查集里面需要有哪些数据结构:

  • 需要一个parent[]数组,用来存储每个元素对应的根节点。
  • 再来一个rank[]数组,代表以每个元素作为根节点,其所在集合的大小。即代表某个集合的深度。
  • 再来一个sum字段,代表当前的集合个数。
public class UnionFind {
    /**
     * 表示节点i的父节点
     */
    private int[] parent;
    /**
     * 表示以节点i为根节点的子树的深度,初始时每个节点的深度都为0
     */
    private int[] rank;
    private int sum;

    public UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        // 初始时每个节点的父节点都是它自己
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
        sum = n;
    }
}

1.2 find 查找函数

特征:

  • 入参:元素x
  • 要做的事情:不断地向上递归寻找这个x的根节点。
  • 递归终止条件:找到根节点。(根节点和元素本身一致)

代码如下:

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

1.3 union 合并集合

特征:

  • 入参:元素xy
  • 要做的事情:分别找到这两个元素的根节点:rootXrootY
  • 如果俩元素的根节点是同一个,说明他们在一个集合当中,不需要任何操作。
  • 倘若两个元素的根节点不一样,根据两个集合的深度来判断。将深度小的那个集合,合并到深度大的集合中。同时更新对应的根节点和深度大小。

除此之外,我们还可以写一个简单的函数,用来判断两个元素是否处于同一个集合当中(或者是是否相连)

public void union(int x, int y) {
    int rootX = find(x);
    int rootY = find(y);
    // 如果两个元素的根节点一致,不需要合并
    if (rootX == rootY) {
        return;
    }
    // 如果根节点 rootX 的深度 > rootY。
    if (rank[rootX] > rank[rootY]) {
        // 那么将以rootY作为根节点的集合加入到rootX对应的集合当中
        rank[rootX] += rank[rootY];
        // 同时改变rootY的根节点,指向rootX。
        parent[rootY] = rootX;
    } else {
        // 反之
        rank[rootY] += rank[rootX];
        parent[rootX] = rootY;
    }
}

1.4 connected 判断相连性

/**
 * 判断两个节点是否在同一个集合中
*/
public boolean connected(int x, int y) {
    return find(x) == find(y);
}

1.5 完整代码

/**
 * @author Zong0915
 * @date 2023/10/4 下午2:52
 */
public class UnionFind {
    /**
     * 表示节点i的父节点
     */
    private int[] parent;
    /**
     * 表示以节点i为根节点的子树的深度,初始时每个节点的深度都为0
     */
    private int[] rank;
    private int sum;

    public UnionFind(int n) {
        parent = new int[n];
        rank = new int[n];
        // 初始时每个节点的父节点都是它自己
        for (int i = 0; i < n; i++) {
            parent[i] = i;
        }
        sum = n;
    }

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

    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        // 如果两个元素的根节点一致,不需要合并
        if (rootX == rootY) {
            return;
        }
        // 如果根节点 rootX 的深度 > rootY。
        if (rank[rootX] > rank[rootY]) {
            // 那么将以rootY作为根节点的集合加入到rootX对应的集合当中
            rank[rootX] += rank[rootY];
            // 同时改变rootY的根节点,指向rootX。
            parent[rootY] = rootX;
        } else {
            // 反之
            rank[rootY] += rank[rootX];
            parent[rootX] = rootY;
        }
    }

    /**
     * 判断两个节点是否在同一个集合中
     */
    public boolean connected(int x, int y) {
        return find(x) == find(y);
    }
}

二. 运用案例 - 省份数量

原题链接
在这里插入图片描述
我们在并查集模板的基础上进行改造:

class UnionFind {
    private int[] rank;// 每个省份具有的城市数量
    private int[] parent;// 每个城市对应的根节点(省份)
    private int sum;// 省份的数量

    public UnionFind(int[][] isConnected) {
        int len = isConnected.length;
        // 初始化,省份数量和提供的城市数量一致
        sum = len;
        // 每个集合具有的城市数量为1
        rank = new int[len];
        parent = new int[len];
        Arrays.fill(rank, 1);
        // 根节点指向自己
        for (int i = 0; i < len; i++) {
            parent[i] = i;
        }
    }

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

    public void union(int x, int y) {
        int rootX = find(x);
        int rootY = find(y);
        // 如果两个元素的根节点一致,不需要合并
        if (rootX == rootY) {
            return;
        }
        // 如果根节点 rootX 的深度 > rootY。
        if (rank[rootX] > rank[rootY]) {
            // 那么将以rootY作为根节点的集合加入到rootX对应的集合当中
            rank[rootX] += rank[rootY];
            // 同时改变rootY的根节点,指向rootX。
            parent[rootY] = rootX;
        } else {
            // 反之
            rank[rootY] += rank[rootX];
            parent[rootX] = rootY;
        }
        // 合并成功,那么总集合数量要减1
        sum--;
    }
}

不过本题目当中,对于rank这个属性没有什么作用,最终看的是sum属性。因此大家可以把这个属性相关的给去除。

最后来看代码部分:

public int findCircleNum(int[][] isConnected) {
	// 初始化构造
    UnionFind unionFind = new UnionFind(isConnected);
    int len1 = isConnected.length;
    int len2 = isConnected[0].length;
    for (int i = 0; i < len1; i++) {
        for (int j = 0; j < len2; j++) {
            // 如果是相连的,那么将城市 i 和 j 合并
            if (isConnected[i][j] == 1) {
                unionFind.union(i, j);
            }
        }
    }
    // 最后返回集合个数(即省份的个数)
    return unionFind.sum;
}

最终代码如下:

public class Test547 {
    public int findCircleNum(int[][] isConnected) {
        UnionFind unionFind = new UnionFind(isConnected);
        int len1 = isConnected.length;
        int len2 = isConnected[0].length;
        for (int i = 0; i < len1; i++) {
            for (int j = 0; j < len2; j++) {
                // 如果是相连的,那么将城市 i 和 j 合并
                if (isConnected[i][j] == 1) {
                    unionFind.union(i, j);
                }
            }
        }
        return unionFind.sum;
    }

    class UnionFind {
        private int[] rank;// 每个省份具有的城市数量
        private int[] parent;// 每个城市对应的根节点(省份)
        private int sum;// 省份的数量

        public UnionFind(int[][] isConnected) {
            int len = isConnected.length;
            // 初始化,省份数量和提供的城市数量一致
            sum = len;
            // 每个集合具有的城市数量为1
            rank = new int[len];
            parent = new int[len];
            Arrays.fill(rank, 1);
            // 根节点指向自己
            for (int i = 0; i < len; i++) {
                parent[i] = i;
            }
        }

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

        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            // 如果两个元素的根节点一致,不需要合并
            if (rootX == rootY) {
                return;
            }
            // 如果根节点 rootX 的深度 > rootY。
            if (rank[rootX] > rank[rootY]) {
                // 那么将以rootY作为根节点的集合加入到rootX对应的集合当中
                rank[rootX] += rank[rootY];
                // 同时改变rootY的根节点,指向rootX。
                parent[rootY] = rootX;
            } else {
                // 反之
                rank[rootY] += rank[rootX];
                parent[rootX] = rootY;
            }
            sum--;
        }
    }
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Zong_0915

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值