开启我的LeetCode刷题日记:1994. 好子集的数目

184 阅读3分钟

「这是我参与2022首次更文挑战的第24天,活动详情查看:2022首次更文挑战

编程世界总是离不了算法

最近在看框架源码时,会有很多算法的实现逻辑,有时候会感到吃力

于是决定蹭着假期,加强算法和数据结构相关的知识

那怎么提升呢?

其实我知道算法这东西没有捷径,多写多练才能提升,于是我开启我的LeetCode刷题之旅

第一阶段目标是:200道,每天12

为了不乱,本系列文章目录分为三部分:

  1. 今日题目:xxx
  2. 我的思路
  3. 代码实现

今天题目:1994. 好子集的数目

给你一个整数数组 nums 。如果 nums 的一个子集中,所有元素的乘积可以表示为一个或多个 互不相同的质数 的乘积,那么我们称它为 好子集 。

比方说,如果 nums = [1, 2, 3, 4] : [2, 3] ,[1, 2, 3] 和 [1, 3] 是 好 子集,乘积分别为 6 = 23 ,6 = 23 和 3 = 3 。 [1, 4] 和 [4] 不是 好 子集,因为乘积分别为 4 = 22 和 4 = 22 。 请你返回 nums 中不同的 好 子集的数目对 109 + 7 取余 的结果。

nums 中的 子集 是通过删除 nums 中一些(可能一个都不删除,也可能全部都删除)元素后剩余元素组成的数组。如果两个子集删除的下标不同,那么它们被视为不同的子集。

 

示例 1:

输入:nums = [1,2,3,4] 输出:6 解释:好子集为:

  • [1,2]:乘积为 2 ,可以表示为质数 2 的乘积。
  • [1,2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。
  • [1,3]:乘积为 3 ,可以表示为质数 3 的乘积。
  • [2]:乘积为 2 ,可以表示为质数 2 的乘积。
  • [2,3]:乘积为 6 ,可以表示为互不相同的质数 2 和 3 的乘积。
  • [3]:乘积为 3 ,可以表示为质数 3 的乘积。 示例 2:

输入:nums = [4,2,3,15] 输出:5 解释:好子集为:

  • [2]:乘积为 2 ,可以表示为质数 2 的乘积。
  • [2,3]:乘积为 6 ,可以表示为互不相同质数 2 和 3 的乘积。
  • [2,15]:乘积为 30 ,可以表示为互不相同质数 2,3 和 5 的乘积。
  • [3]:乘积为 3 ,可以表示为质数 3 的乘积。
  • [15]:乘积为 15 ,可以表示为互不相同质数 3 和 5 的乘积。  

提示:

1 <= nums.length <= 105 1 <= nums[i] <= 30

我的思路

如何判断一个子集是否好子集

暴力法:对每个元素进行分解质因数,然后再判断有没重复的质因数。

预先分解质因数,可以避免重复计算,此时每个数字能得到一个质因数集合。

数据范围是 1 <= nums[i] <= 30, 因此可以将质因数集合压缩到一个 int 内,每一个二进制位代表该质数是否出现,例如 10 = 2 * 5, 质因数集合可用二进制数 10010 表示。将多个集合进行与运算便可得知是否存在重复质因数。

因为 30 以内的质数只有 10 个,可以进一步压缩质因数集合,此时(从右到左)第 i 个二进制位代表第 i 个质数是否出现,例如 21 = 3 * 7,可用二进制数 1010 表示。此压缩方式不必须,但是可以容纳更大的输入范围。

遍历所有的好子集

要拆分为互不相同的质数的乘积,除了 1, 必然不存在重复数字,因此可以统计出现次数,合并重复数字。 使用回溯法,从 2 到 30, 尝试所有数字的组合,每个数字都有选取和不选取两条分支。

统计好子集的数量

选取某个数字时,可以选取每一个出现该数字的位置,因此有出现次数种可能性。

将该子集各个数字的可能性相乘即可得到该子集的数量。

部分数字,例如 4 = 2 * 2, 必然不会选取,可以先排除。

计数为 0 或质因数集合有重复时,可排除选取分支。

需要减去不选取任何数字的情况。

每个 1 可提供两种可能性。

代码实现

/**
 * @param {number[]} nums
 * @return {number}
 */
var numberOfGoodSubsets = function (nums) {
  // 可以组合的数
  const sets = [2, 3, 5, 6, 7, 10, 11, 13, 14, 15, 17, 19, 21, 22, 23, 26, 29, 30];
  // 代表质数乘积的状态位
  const bits = [1, 2, 4, 3, 8, 5, 16, 32, 9, 6, 64, 128, 10, 17, 256, 33, 512, 7];
  const mod = 1000000007;
  const freq = new Array(31).fill(0);
  for (const n of nums) freq[n]++;

  // 对每个可以组合的数进行遍历
  let [acc, state, count] = [-1, 0, 1];
  (function backtrack(i) {
    // 得到一种组合方式,统计数量
    if (i >= sets.length) return acc = (acc + count) % mod;

    // 不取这个数
    backtrack(i + 1);

    const [n, b] = [freq[sets[i]], bits[i]];
    // 计数为 0, 跳过
    if (!n) return;
    // 无法选取, 跳过
    if (state & b) return;

    // 选取时提供 n 种可能性,记录状态
    let temp = count;
    [count, state] = [count * n % mod, state | b];

    // 递归
    backtrack(i + 1);

    // 回溯,还原状态
    [count, state] = [temp, state ^ b];
  })(0);

  // 每个 1 提供取与不取两种可能
  while (freq[1]--) acc = (acc * 2) % mod;
  return acc;
};



总结

实现方式其实有很多,这里仅供参考~

由于刚开始刷题,也不知道从哪里刷好,如果前辈们有好的建议,希望不吝赐教,感谢🌹