青训营X豆包MarsCode技术训练营第九课|豆包MarsCode刷题

45 阅读5分钟

今天刷的也是一道难度特别难的题目:小E的倍数子集问题

问题描述

小E想知道一个给定集合中,有多少个子集满足以下条件:

  • 子集内的所有元素数量大于 1。
  • 子集内的所有元素两两之间互为倍数关系。

由于结果可能非常大,输出的结果需要对 109+7109+7 取模。


测试样例

样例1:

输入:n = 5,a = [1, 2, 3, 4, 5]
输出:6

样例2:

输入:n = 6,a = [2, 4, 8, 16, 32, 64]
输出:57

样例3:

输入:n = 4,a = [3, 6, 9, 12]
输出:5

好的,下面我将对这道题目进行详细分析,尽可能深入地解释每个步骤和思路。

题目分析

题目要求我们找到一个数组中所有符合以下条件的子集的数量:

  1. 子集内的元素数量必须大于1。
  2. 子集内的元素之间存在倍数关系,即每个元素都是前一个元素的倍数。

问题解析

倍数关系是指,如果一个子集包含了元素 xy,那么它们之间必须满足 x = k * yy = k * x,其中 k 是某个整数。如果我们能够找到满足这个条件的子集,并且子集的大小大于1,就需要统计出来。

直接的暴力方法是枚举所有的子集,但是这样会非常低效。因为给定一个数组的子集数目是 (2^n),而我们需要判断每个子集是否符合倍数关系。暴力解法的时间复杂度非常高,甚至可能导致计算时间超时。因此,我们需要寻找一种更高效的算法。

思路:动态规划 + 排序优化

为了提高效率,我们可以使用动态规划(Dynamic Programming, DP)来解决这个问题。同时,我们利用排序来简化倍数关系的判断。

1. 排序:简化倍数判断

首先,排序是非常关键的一步。排序后,数组中的每个元素都会按照升序排列。这样,如果某个元素是另一个元素的倍数,它必定会出现在它之后(即小的元素会在前面)。因此,我们只需要在动态规划的过程中,检查每个元素之前的所有元素,而不用反向查找。这个顺序保证了我们能够有效地判断倍数关系。

例如,假设我们有一个数组 [1, 2, 3, 4, 6, 8],排序后数组为 [1, 2, 3, 4, 6, 8]。对于元素 8,我们只需要检查 124,因为这些元素都小于 8,并且如果它们是 8 的倍数,我们可以直接用它们来扩展子集。

2. 动态规划(DP)

我们可以定义一个 dp 数组,其中 dp[i] 表示以元素 a[i] 为末尾的倍数链的子集数量。

初始化:每个元素本身可以构成一个子集,所以初始状态下,每个 dp[i] 都为 1。

状态转移:对于每个元素 a[i],我们检查它之前的所有元素 a[j],如果 a[i] 能被 a[j] 整除(即 a[i] % a[j] == 0),那么我们可以把以 a[j] 结尾的所有子集扩展到 a[i]。具体来说,如果 a[i] % a[j] == 0,那么就有 dp[i] = dp[i] + dp[j]

为什么加 dp[j]
这是因为,dp[j] 表示以 a[j] 为结尾的符合倍数关系的子集的数量,所有这些子集都可以通过添加 a[i] 来扩展。由于 a[i]a[j] 的倍数,我们就可以把所有以 a[j] 为结尾的子集都扩展成以 a[i] 为结尾的子集。

3. 最终结果

最终结果应该是所有 dp[i] 的总和减去单元素的子集。因为每个元素本身作为一个子集,初始时 dp[i] = 1,而题目要求的是子集的元素数量必须大于1,因此我们需要减去单个元素子集的个数,也就是 n(数组的长度)。因此,最终结果就是所有 dp[i] 的和减去 n

4. 考虑溢出

由于答案可能会非常大,我们需要对最终结果取模。题目要求输出结果对 (10^9 + 7) 取模。

代码解释

  1. 排序:我们首先对数组进行升序排序。排序后,数组的每个元素都按照升序排列,有利于我们在遍历过程中更容易找到倍数关系。

  2. 初始化:我们为每个元素 a[i] 初始化一个 dp[i],表示以 a[i] 为末尾的符合条件的子集数量。每个元素至少可以自己构成一个子集,因此初始值为 1。

  3. 动态规划填充 dp 数组:我们遍历数组,对于每个元素 a[i],我们检查它之前的所有元素 a[j],如果 a[i] % a[j] == 0,则说明可以将 a[j] 扩展到 a[i],我们就将 dp[j] 的值加到 dp[i]

  4. 最终结果:最终,所有 dp[i] 的值表示以 a[i] 为结尾的符合条件的子集的数量。为了去掉只有一个元素的子集(即每个元素自己本身),我们需要减去 n(数组的长度),因为每个元素的子集计数是 1。

  5. 取模:最后,为了防止结果过大,我们需要对结果进行取模,使用 (10^9 + 7) 作为模数。

时间复杂度分析

  • 排序的时间复杂度是 (O(n \log n))。
  • 动态规划部分的时间复杂度是 (O(n^2)),因为我们在每个元素上都遍历前面的所有元素。

因此,总的时间复杂度是 (O(n^2)),这是由于双重循环检查倍数关系所致。

空间复杂度分析

我们使用了一个大小为 (n) 的 dp 数组来保存每个元素作为子集末尾时的子集数目,因此空间复杂度是 (O(n))。

结论

通过使用排序和动态规划的组合,我们能够高效地解决这个问题。尽管最坏情况下时间复杂度是 (O(n^2)),但对于大多数常见的输入规模,这种方法已经足够高效。