概述
由算数基本定理,我们知道任意一个大于1的自然数可以表示为一些质数的乘积:
LeetCode 2521:
给你一个正整数数组
nums
,对nums
所有元素求积之后,找出并返回乘积中 不同质因数 的数目。注意:
- 质数 是指大于
1
且仅能被1
及自身整除的数字。- 如果
val2 / val1
是一个整数,则整数val1
是另一个整数val2
的一个因数。示例 1:
输入:nums = [2,4,3,7,10,6] 输出:4 解释: nums 中所有元素的乘积是:2 * 4 * 3 * 7 * 10 * 6 = 10080 = 25 * 32 * 5 * 7 。 共有 4 个不同的质因数,所以返回 4 。
思路
质因子:若a可整除x,且a为质数,则a为x的质因子。
明确一件事:
如果x存在大于的质数,则最多只存在一个,否则两个大于的质因数相乘会大于x。
也就是说我们可以质枚举小于的质因子,并在最后单独判断:
int cnt[N]{}; //cnt[i] = n 表示i是x的n个质因子
for (int i = 2; i * i <= x; i++) while(!(x % i)) x /= i, cnt[i]++;
if (x != 1)... //意味着当前x是原始x的最后的那一个大质因子
也就是在循环内部不断消耗可能的质因数,在最后脱离循环时,如果 x!=1,则当前x也为原始x的那个大于的质因数。
可以证明的是,while循环会保证x一定是被他的质因数消耗的。
例如,如果x不断被i=2消耗,则后续i=4时x已经无法再被消耗。
在循环条件中的i * i <= x看起来有点不对劲。由于while中x不断被消耗,这似乎不能保证i能够枚举到原始的终点。这其实不要紧,我们来证明这一点:
设原始x = P1^(K1) * P2^(K2) * ... * Pn^(Kn) * Pt。
(Pi为一系列递增质数,Ki为对应的幂,Pt为唯一的大于根号的质数)
那么当i = P1,while完全消耗P1时,问题就等价于分解剩余的y = P2^(K2) * ... * Pn^(Kn) * Pt。
由于P2>P1,接下来for循环使i从P1增长到P2时完全正确的。依旧,只需要循环i * i小于等于y。
同理,后续的分解也是如此。
在最后,单独判断最后的x,如果它不等于1,则意味着这是那个大质因子。
算法过程
对于本题,题目要求我们分解整个数组的乘积,但是比起先全部相乘再分解,不如逐一分解每个数组元素。
vector<int> cnt(1001);for (int num : nums){for (int i = 2; i <= 1000; i++){while (!(num % i)) cnt[i]++, num /= i;}if (num != 1) cnt[num]++;
}
这种枚举有些低效,怎么优化呢?
优化方案
还记得欧拉筛吗?「数学::质数」埃氏筛|欧拉筛(埃拉托斯特尼筛法|线性筛法)/ LeetCode 204(C++)
预处理是个很好的工作,先获取范围内所有的质数的数组prim,这样for循环的循环次数就大大降低了。
vector<int> cnt(N);
for (int num : nums){for (int i = 0; prim[i] * prim[i] <= num; i++){while (!(num % prim[i])) cnt[prim[i]]++, num /= prim[i];}if (num != 1) cnt[num]++;
}
Code
constexpr int N = 1e3 + 1;
bool not_prim[N]{};
vector<int> prim;
auto init = []() -> int {for (int i = 2; i < N; i++) {if (!not_prim[i]) prim.push_back(i);for (int j = 0; i * prim[j] < N; j++) {not_prim[i * prim[j]] = true;if (!(i % prim[j])) break;}}return 0;
}();
class Solution {
public:int distinctPrimeFactors(vector<int>& nums) {vector<int> cnt(N);for (int num : nums){for (int i = 0; prim[i] * prim[i] <= num; i++){while (!(num % prim[i])) cnt[prim[i]]++, num /= prim[i];}if (num != 1) cnt[num]++;}return count_if(cnt.begin(), cnt.end(), [](int num) -> bool {return num;});}
};
复杂度
时间复杂度: O(nlogn) //分解单个数为logn
空间复杂度: O(n) //预处理prim数组
总结
算术基本定理是初等数论中一条非常基本和重要的定理,它把对自然数的研究转化为对其最基本的元素——素数的研究。不觉得很酷吗?