leetcode
leetcode 851 ~ 900
按公因数计算最大组件大小

按公因数计算最大组件大小

难度:

标签:

题目描述

给定一个由不同正整数的组成的非空数组 nums ,考虑下面的图:

  • 有 nums.length 个节点,按从 nums[0] 到 nums[nums.length - 1] 标记;
  • 只有当 nums[i] 和 nums[j] 共用一个大于 1 的公因数时,nums[i] 和 nums[j]之间才有一条边。

返回 图中最大连通组件的大小

 

示例 1:

输入:nums = [4,6,15,35]
输出:4

示例 2:

输入:nums = [20,50,9,63]
输出:2

示例 3:

输入:nums = [2,3,6,7,4,12,21,39]
输出:8

 

提示:

  • 1 <= nums.length <= 2 * 104
  • 1 <= nums[i] <= 105
  • nums 中所有值都 不同

代码结果

运行时间: 304 ms, 内存: 30.3 MB


/*
题目思路:
1. 我们需要找到图中最大的连通组件的大小,图中的节点由数组nums中的元素表示,
   如果两个元素有一个大于1的公因数,那么它们之间有一条边。
2. 为了求解,我们可以使用并查集(Union-Find)来跟踪每个节点所属的连通分量。
3. 对于每个数,我们找到它的所有因数,并将这些因数所在的集合进行合并。
4. 最终,我们通过查找每个集合的大小来确定最大的连通组件的大小。
*/

import java.util.*;
import java.util.stream.IntStream;
import java.util.stream.Stream;

public class LargestComponentSizeStream {
    public int largestComponentSize(int[] nums) {
        int max = Arrays.stream(nums).max().orElse(0);
        UnionFind uf = new UnionFind(max + 1);

        Arrays.stream(nums).forEach(num -> {
            IntStream.rangeClosed(2, (int) Math.sqrt(num)).filter(factor -> num % factor == 0).forEach(factor -> {
                uf.union(num, factor);
                uf.union(num, num / factor);
            });
        });

        Map<Integer, Long> count = Arrays.stream(nums).boxed()
            .collect(Collectors.groupingBy(uf::find, Collectors.counting()));

        return count.values().stream().max(Long::compareTo).orElse(0L).intValue();
    }

    class UnionFind {
        int[] parent;
        int[] rank;

        public UnionFind(int size) {
            parent = new int[size];
            rank = new int[size];
            IntStream.range(0, size).forEach(i -> parent[i] = i);
        }

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

        public void union(int x, int y) {
            int rootX = find(x);
            int rootY = find(y);
            if (rootX != rootY) {
                if (rank[rootX] > rank[rootY]) {
                    parent[rootY] = rootX;
                } else if (rank[rootX] < rank[rootY]) {
                    parent[rootX] = rootY;
                } else {
                    parent[rootY] = rootX;
                    rank[rootX]++;
                }
            }
        }
    }
}

解释

方法:

题解采用了素数筛法结合并查集的方法来解决问题。首先,使用埃拉托斯特尼筛法预先计算出所有小于等于100000的素数,并为每个数标记它的素数因子。然后,对于每个输入数字,通过它的素数因子来定义它在图中的节点。如果两个数有共同的素数因子,它们在图中就是相连的。这样,问题转化为了找出具有共同素数因子的最大连通组件。使用并查集来处理节点的连接和组件的合并。最后,遍历输入数组,对每个数的素数因子进行并查集合并操作,最终通过计数并查集根的频率来确定最大的连通组件大小。

时间复杂度:

O(n log log n + m log n)

空间复杂度:

O(n + m)

代码细节讲解

🦆
在算法中,为什么选择使用埃拉托斯特尼筛法而不是其他方法来找出所有素数?
埃拉托斯特尼筛法(Sieve of Eratosthenes)是一种高效且简单的方法来找出小于或等于给定数的所有素数。该方法的时间复杂度为O(n log log n),比其他简单方法更快,特别是当需要处理大量数据时。此外,它的空间效率也很高,只需要一个布尔数组来标记非素数。这些特点使得埃拉托斯特尼筛法在处理需要预先计算大量素数的问题时非常合适,这也是为什么在这个题解中选择使用它来预处理素数。
🦆
并查集在此题解中的具体作用是什么?如何通过并查集找到最大的连通组件?
并查集是一种数据结构,它可以高效地处理一些不交集的合并及查询问题。在本题中,使用并查集来管理节点(数的素数因子)的连接状态,从而快速确定哪些数是通过共享素数因子相互连接的。算法中,每次两个数的素数因子发现有共同项时,就通过并查集的`link`操作将它们所属的集合合并。最后,通过遍历所有数的素数因子,并查集中的根节点的频率,可以找到最大的连通组件的大小。这种方法能够有效地将问题从直接处理数转变为处理数的素数分解,大大简化了问题的复杂度。
🦆
题中提到的并查集的`link`操作中,为什么要比较`rank`并根据比较结果选择不同的合并策略?
在并查集中,`rank`通常用来表示每个节点的树的深度(或近似深度)。比较`rank`并根据它选择合并策略的方法被称为按秩合并。该策略的目的是尽可能保持树的深度小,从而优化并查集的操作效率。具体来说,如果两个节点的树的`rank`不同,我们将深度较小的树连接到深度较大的树上;如果它们的深度相同,则选择其中一个作为根,并将其`rank`增加1。这种方法可以减少路径长度,进一步优化查找的时间复杂度,使并查集的操作更加高效。

相关问题