【前端刷力扣】LeetCode 3411. 最长乘积等价子数组(easy题目的hard解法)

258 阅读5分钟

题目描述

给你一个由正整数组成的数组 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]) = 2gcd([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 <= 100
  • 1 <= 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 是质数,xiyi 是对应的指数(可以为 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

解题思路

方法一:暴力枚举所有子数组

因为题目的数据很小,可以直接枚举全部情况,然后判断是否满足条件,最后找出最长的子数组长度作为答案。

注意:全部数据的乘积可能很大,直接计算会溢出。我们可以提前计算出最大可能的乘积:

  1. 最大可能的 lcm 为所有数字的最小公倍数
  2. 最大可能的 gcd 为数组中最大的数字
  3. 当子数组的乘积大于 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;
};

方法二:滑动窗口

通过分析质因数分解,我们可以得到一个重要结论:

  1. 当子数组长度为 2 时,一定满足 prod = lcm * gcd (上面最小公倍数部分已经论证过了,不再重复论证)
  2. 当子数组长度 ≥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;
};