携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第4天,点击查看活动详情
题目描述
给定一个由不同正整数的组成的非空数组 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的公因数才能被连通,即连通组件中相邻的两个元素都一定会有公因数,涉及到连通组件的操作可以采用并查集;
- 查找公因数,首先要查找范围内的数的所有的质数。整数分为合数、质数、1.合数是指除了本身和1之外还有其它的因数。质数是指除了本身和1外没有其它的因数;
- 所以题目给定的数组如果包含质因数,则这个节点一定不能被其它节点连通。除此之外,其它所有的节点想要连通,那么两个节点之间一定有一个公共质因数。所以这道题的难点在于计算质因数和连通查找。
解题思路
- 欧拉筛 找到一个素数后,就将它的倍数标记为合数,也就是把它的倍数“筛掉”;如果一个数没有被比它小的素数“筛掉”,那它就是素数。欧拉筛的区别就是只被最小质数“筛掉”。
- 并查集 没有交集的集合的操作,包括将两个集合合并和判断两个节点是否在同一个集合里。主要用于在图类算法中计算动态连通性。
代码实现
public int largestComponentSize(int[] nums) {
int maxNum = Arrays.stream(nums).max().getAsInt();
// 存放改下标对应的值是否是质数,如primeFlag[2] 代表2是否是质数,true为合数,false为质数
boolean[] primeFlag = new boolean[maxNum + 1];
// 存放(2,maxNum)之间所有的质数
int[] primes = new int[maxNum];
for (int i=2; i <= maxNum;i ++) {
// 查找范围内的质数
if (!primeFlag[i]){
// 放入质数中 primes[0]用于存储数组primes的真实长度,因为在创建时无法确定数据的质数个数
primes[++primes[0]] = i;
}
// 如果当前节点是质数,则它的倍数都不是质数
// 遍历已存在的质数,用当前节点乘以质数,获取到的数一定不是质数,终止条件为primes数组长度且这个乘积小于最大数(大于的数无意义)
for (int j = 1;j <= primes[0] && i * primes[j] <= maxNum;j++){
// 置为合数
primeFlag[i * primes[j]] = true;
// 欧拉筛只被它的最小质因数筛出,所以当下一个质数能够被当前节点整除时,代表当前已经不是最小质因数,可以直接进行下一次质数筛出
if (i % primes[j] == 0) {
break;
}
}
}
// 初始化并查集
UnionFind unionFind = new UnionFind(maxNum);
// 遍历元素,将元素与质因数合并起来
for (int num : nums) {
int quotient = num;
for (int j=1; j < primes[0] & primes[j] * primes[j] <= quotient; j++) {
if (num % primes[j] == 0) {
unionFind.union(num,primes[j]);
while (quotient % primes[j] == 0) {
quotient /= primes[j];
}
}
}
if (quotient > 1) {
unionFind.union(quotient, num);
}
}
int[] sizeCount = new int[maxNum + 1];
int maxSize = 0;
for (int num : nums) {
//计算每个合集中元素出现的次数
maxSize = Math.max(maxSize,++ sizeCount[unionFind.find(num)]);
}
return maxSize;
}
public class UnionFind{
// 每个节点对应的集合大小
int[] size;
// 存放每个节点的父节点
int[] parent;
/**
* 将两个节点所属集合合并
*
* @param p 节点p
* @param q 节点q
*/
public void union(int p,int q){
int topP = find(p);
int topQ = find(q);
if (topQ == topP){
// 集合相同,无需合并
return;
}
// 权衡集合大小,进行集合合并
if (size[topP] > size[topQ]){
// p集合的数量大于q
parent[topP] = topQ;
++size[topQ];
}else {
parent[topQ] = topP;
++size[topP];
}
}
/**
* 查找节点p所在的集合的顶级父节点
*
* @param p 节点p
* @return 节点p所在集合的顶级父节点
*/
private int find(int p) {
// 递归获取顶级节点
// 路径压缩,将当前节点的父节点直接设置成父节点的父节点
return parent[p] == p ? p : (parent[p] = find(parent[p]));
}
public UnionFind(int n){
parent = new int[n+1];
size = new int[n+1];
for (int i = 0 ; i <= n ; i++) {
parent[i] = i;
size[i] = 1;
}
}
}