LeetCode上的952. 按公因数计算最大组件大小,难度:困难(2272)
问题描述
给定一个由不同正整数的组成的非空数组 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 * 1041 <= nums[i] <= 105nums中所有值都 不同
预备知识
1)埃氏筛
考虑这样一件事情:对于任意一个大于1的正整数n,那么它的 x 倍就是合数(x > 1)。利用这个结论,我们可以避免很多次不必要的检测。
如果我们从小到大考虑每个数,然后同时把当前这个数的所有(比自己大的)倍数记为合数,那么运行结束的时候没有被标记的数就是素数了。
int Eratosthenes(int n) {
int[] prime = new int[n];
boolean[] is_prime = new boolean[n];
// 初始每个数字都是质数
Arrays.fill(is_prime, true);
int p = 0;
// 0&1不是质数
is_prime[0] = is_prime[1] = false;
for (int i = 2; i <= n; ++i) {
if (is_prime[i]) {
prime[p++] = i; // prime[p]是i,后置自增运算代表当前素数数量
// 这个是一个优化的点,因为i * i之前的非质数已经被之前的质数筛出去了
// 【比如3 - 9之前的6】
if (i * i <= n)
for (int j = i * i; j <= n; j += i)
// 因为从 2 到 i - 1 的倍数我们之前筛过了,这里直接从 i
// 的倍数开始,提高了运行速度
is_prime[j] = false; // 是i的倍数的均不是素数
}
}
return p;
}
2)线性筛(欧拉筛)
埃氏筛法仍有优化空间,它会将一个合数重复多次标记。如果能让每个合数都只被标记一次,那么时间复杂度就可以降到 O(n) 了。
比如12,会被2&3标记一次
思想:上面提到每个合数都只被标记一次,那么应该是被它的最小质因子标记的。比如12的最小质因子是2,不应该由4*3标记,而应该由2*6标记
综上所述,遍历每一个数字,只要求标记当前数字乘上比当前数字小于等于的质数的结果就可以了
线性筛同时也得到了每个数的最小质因子
编码过程
- 遍历1-n,当前数字为i,当遇到第一个i % j == 0的时候()
// 标记每个数字是否是质数
public static int[] isPrime = new int[n];
// 保存所有质数
public static int[] primes = new int[n];
public int euler(int[] nums) {
//欧拉筛,找出2-n的所有质数
for (int i = 2; i < n; i++) {
if (isPrime[i] == 0) {
primes[k++] = i;
}
// 遍历所有的质数,这个primes[j]一定是小于等于i的
for (int j = 0; primes[j] * i < n; j++) {
isPrime[primes[j] * i] = 1;
// i只与比它的最小质因子的质数相乘,否则也会重复筛选
// 比如12应该由2*6筛选掉,而不是由3*4,保证4只乘到2就可以了
if (i % primes[j] == 0) {
break;
}
}
}
}
欧拉筛+并查集
注释的很清楚了
class Solution {
public static int n = (int) 1e5 + 7;
public static int[] isPrime = new int[n];
// 记录了所有的素数
public static int[] primes = new int[n];
//并查集
public static int[] parent = new int[n];
int k = 0;
public int largestComponentSize(int[] nums) {
//欧拉筛,找出1-n的所有质数
for (int i = 2; i < n; i++) {
if (isPrime[i] == 0) {
primes[k++] = i;
}
for (int j = 0; primes[j] * i < n; j++) {
isPrime[primes[j] * i] = 1;
if (i % primes[j] == 0) {
break;
}
}
}
//初始化并查集
for (int i = 0; i < n; i++) {
parent[i] = i;
}
//遍历nums中的每个数,和他们的质因数连接
for (int num : nums) {
int quotient = num;
// 遍历primes,找到对应的质因数
for (int j = 0; j < k && primes[j] * primes[j] <= quotient; j++) {
if (quotient % primes[j] == 0) {
// primes[i]是他的质因数
union(num, primes[j]);
// 一个正整数一定可以由多个质因数的幂相加得到,下面的操作相当于把primes[j]的幂除掉
// 比如17 = 2^3 + 3^2
while (quotient % primes[j] == 0) {
quotient /= primes[j];
}
}
}
//假如剩下了一个质因数,也和num连接,使得不同的质因数可以联合到一起
//这种情况是因为num是一个合数 由不同的质因数相乘组成 把他的质因数连接起来
if (quotient > 1) {
union(quotient, num);
}
}
// 统计数组
int[] cnt = new int[n];
int ans = 0;
//遍历nums,判断是否属于某个根
for (int num : nums) {
// 如果属于同一个根,直接++,然后判断最大值
ans = Math.max(ans, ++cnt[find(num)]);
}
return ans;
}
public void union(int x, int y) {
int parentX = find(x);
int parentY = find(y);
if (parentX != parentY) {
parent[parentX] = parentY;
}
}
public int find(int x) {
// 采用路径压缩
return parent[x] == x ? x : (parent[x] = find(parent[x]));
}
}