题目描述
给你一个由正整数组成的数组 nums。
如果一个数组 arr 满足 prod(arr) == lcm(arr) * gcd(arr) ,则称其为乘积等价数组:
prod(arr)表示 arr 中所有元素的乘积。gcd(arr)表示 arr 中所有元素的最大公因数。lcm(arr)表示 arr 中所有元素的最小公倍数。
返回 nums 中最长的乘积等价子数组的长度。
示例 1:
输入: nums = [1,2,1,2,1,1,1]
输出: 5
解释:
最长的乘积等价子数组是 [1, 2, 1, 1, 1],其中 prod([1, 2, 1, 1, 1]) = 2, gcd([1, 2, 1, 1, 1]) = 1,以及 lcm([1, 2, 1, 1, 1]) = 2。
示例 2:
输入: nums = [2,3,4,5,6]
输出: 3
解释:
最长的乘积等价子数组是 [3, 4, 5]。
示例 3:
输入: nums = [1,2,3,1,4,5,1]
输出: 5
提示:
2 <= nums.length <= 1001 <= nums[i] <= 10
前置知识
最大公约数(GCD)
最大公约数(Greatest Common Divisor)是指能够整除两个或多个整数的最大正整数。例如,8 和 12 的最大公约数是 4。
计算两个数的最大公约数可用欧几里得算法(辗转相除法):
function gcd(a, b) {
return b === 0 ? a : gcd(b, a % b);
}
最小公倍数(LCM)
最小公倍数(Least Common Multiple)是指能够被两个或多个整数整除的最小正整数。例如,8 和 12 的最小公倍数是 24。
因为两个数的乘积等于它们的最大公约数与最小公倍数的乘积,最小公倍数可以通过最大公约数来计算:
function lcm(a, b) {
return Math.floor((a * b) / gcd(a, b));
}
为什么 a * b = gcd(a, b) * lcm(a, b)?
-
首先,任何正整数都可以表示为质因数的乘积:
设
a = p1^x1 * p2^x2 * ... * pn^xn, b = p1^y1 * p2^y2 * ... * pn^yn其中
pi是质数,xi和yi是对应的指数(可以为 0) -
那么
a * b就是a * b = p1^(x1+y1) * p2^(x2+y2) * ... * pn^(xn+yn) -
对于最大公约数
gcd(a,b)取每个质因数的最小指数:gcd(a,b) = p1^min(x1,y1) * p2^min(x2,y2) * ... * pn^min(xn,yn) -
对于最小公倍数
lcm(a,b),取每个质因数的最大指数lcm(a,b) = p1^max(x1,y1) * p2^max(x2,y2) * ... * pn^max(xn,yn) -
因此
gcd(a,b) * lcm(a,b)就是:p1^(min(x1,y1)+max(x1,y1)) * p2^(min(x2,y2)+max(x2,y2)) * ... * pn^(min(xn,yn)+max(xn,yn))对于任意两个数,
min(x,y) + max(x,y) = x + y所以
gcd(a,b) * lcm(a,b) = p1^(x1+y1) * p2^(x2+y2) * ... * pn^(xn+yn)即
gcd(a,b) * lcm(a,b) = a * b
解题思路
方法一:暴力枚举所有子数组
因为题目的数据很小,可以直接枚举全部情况,然后判断是否满足条件,最后找出最长的子数组长度作为答案。
注意:全部数据的乘积可能很大,直接计算会溢出。我们可以提前计算出最大可能的乘积:
- 最大可能的 lcm 为所有数字的最小公倍数
- 最大可能的 gcd 为数组中最大的数字
- 当子数组的乘积大于
maxLcm * maxGcd时,等式不可能会成立,可以提前结束计算
代码实现
// 计算最大公约数
const gcd = (a, b) => {
return b === 0 ? a : gcd(b, a % b);
};
// 计算最小公倍数
const lcm = (a, b) => {
return Math.floor(a / gcd(a, b)) * b;
};
/**
* @param {number[]} nums
* @return {number}
*/
var maxLength = function (nums) {
// 计算最大可能的lcm为所有数字的lcm
const maxLcm = nums.reduce((a, b) => lcm(a, b), 1);
// 计算最大可能的gcd为数组中最大的数字
const maxGcd = Math.max(...nums);
const n = nums.length;
let ans = 1;
for (let i = 0; i < n; i++) {
let prod = 1;
let g = nums[i]; // 子数组的最大公约数
let l = nums[i]; // 子数组的最小公倍数
for (let j = i; j < n; j++) {
prod = prod * nums[j];
g = gcd(g, nums[j]);
l = lcm(l, nums[j]);
// 如果乘积大于最大可能的乘积,则不再继续计算
if (prod > maxLcm * maxGcd) break;
// 检查是否满足乘积等价条件
if (prod === l * g) {
ans = Math.max(ans, j - i + 1);
}
}
}
return ans;
};
方法二:滑动窗口
通过分析质因数分解,我们可以得到一个重要结论:
- 当子数组长度为 2 时,一定满足
prod = lcm * gcd(上面最小公倍数部分已经论证过了,不再重复论证) - 当子数组长度 ≥3 时,数组中的元素必须两两互质才能满足条件
prod(arr) = lcm(arr) * gcd(arr),论证如下:
1. 假设数组中有三个数 a, b, c,它们不是两两互质:
- 设 gcd(a, b) > 1,记为 d
- 即 a = k1 * d, b = k2 * d(k1,k2为整数)
2. 分析三个数的情况:
- prod = a * b * c = (k1*d) * (k2*d) * c = k1 * k2 * d^2 * c
- gcd = gcd(gcd(a,b),c) = gcd(d,c)
- lcm = lcm(lcm(a,b),c) = lcm(k1*k2*d,c)
3. 如果 gcd(d,c) = 1,则:
- prod = k1 * k2 * d^2 * c
- gcd = 1
- lcm = k1 * k2 * d * c
- 此时 prod = lcm * gcd * d, d > 1,所以 prod = lcm * gcd 不成立
4. 如果 gcd(d,c) > 1,设 gcd(d,c) = g,则:
- prod = k1 * k2 * d^2 * c
- gcd = g
- lcm = k1 * k2 * d * c / g
- 此时 prod = lcm * gcd * d, d > 1,所以 prod = lcm * gcd 不成立
5. 相反,如果数组元素两两互质,则:
- 对于任意两个数 x, y,有 gcd(x,y) = 1
- 此时整个数组的 gcd = 1
- lcm 将等于所有数的乘积
- 因此 prod = lcm * gcd 成立
因此,当数组长度≥3时,要满足乘积等价条件,数组元素必须两两互质。
现在题目变成了查找最长的数组元素两两互质的子数组,所以可以通过滑动窗口来实现。
代码实现
/**
* @param {number[]} nums
* @return {number}
*/
var maxLength = function(nums) {
// 计算最大公约数
const gcd = (a, b) => b === 0 ? a : gcd(b, a % b);
let ans = 2;
let l = 0, prod = 1; // l窗口左边界 prod子数组乘积
for (let r = 0; r < nums.length; r++) {
// 如果子数组不满足两两互质,则缩小窗口
while (gcd(prod, nums[r]) > 1) {
prod /= nums[l];
l++;
}
// 窗口右移,更新乘积
prod *= nums[r];
ans = Math.max(ans, r - l + 1);
}
return ans;
};