今天刷的也是一道难度特别难的题目:小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。
- 子集内的元素之间存在倍数关系,即每个元素都是前一个元素的倍数。
问题解析
倍数关系是指,如果一个子集包含了元素 x 和 y,那么它们之间必须满足 x = k * y 或 y = k * x,其中 k 是某个整数。如果我们能够找到满足这个条件的子集,并且子集的大小大于1,就需要统计出来。
直接的暴力方法是枚举所有的子集,但是这样会非常低效。因为给定一个数组的子集数目是 (2^n),而我们需要判断每个子集是否符合倍数关系。暴力解法的时间复杂度非常高,甚至可能导致计算时间超时。因此,我们需要寻找一种更高效的算法。
思路:动态规划 + 排序优化
为了提高效率,我们可以使用动态规划(Dynamic Programming, DP)来解决这个问题。同时,我们利用排序来简化倍数关系的判断。
1. 排序:简化倍数判断
首先,排序是非常关键的一步。排序后,数组中的每个元素都会按照升序排列。这样,如果某个元素是另一个元素的倍数,它必定会出现在它之后(即小的元素会在前面)。因此,我们只需要在动态规划的过程中,检查每个元素之前的所有元素,而不用反向查找。这个顺序保证了我们能够有效地判断倍数关系。
例如,假设我们有一个数组 [1, 2, 3, 4, 6, 8],排序后数组为 [1, 2, 3, 4, 6, 8]。对于元素 8,我们只需要检查 1、2、4,因为这些元素都小于 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) 取模。
代码解释
-
排序:我们首先对数组进行升序排序。排序后,数组的每个元素都按照升序排列,有利于我们在遍历过程中更容易找到倍数关系。
-
初始化:我们为每个元素
a[i]初始化一个dp[i],表示以a[i]为末尾的符合条件的子集数量。每个元素至少可以自己构成一个子集,因此初始值为 1。 -
动态规划填充
dp数组:我们遍历数组,对于每个元素a[i],我们检查它之前的所有元素a[j],如果a[i] % a[j] == 0,则说明可以将a[j]扩展到a[i],我们就将dp[j]的值加到dp[i]。 -
最终结果:最终,所有
dp[i]的值表示以a[i]为结尾的符合条件的子集的数量。为了去掉只有一个元素的子集(即每个元素自己本身),我们需要减去n(数组的长度),因为每个元素的子集计数是 1。 -
取模:最后,为了防止结果过大,我们需要对结果进行取模,使用 (10^9 + 7) 作为模数。
时间复杂度分析
- 排序的时间复杂度是 (O(n \log n))。
- 动态规划部分的时间复杂度是 (O(n^2)),因为我们在每个元素上都遍历前面的所有元素。
因此,总的时间复杂度是 (O(n^2)),这是由于双重循环检查倍数关系所致。
空间复杂度分析
我们使用了一个大小为 (n) 的 dp 数组来保存每个元素作为子集末尾时的子集数目,因此空间复杂度是 (O(n))。
结论
通过使用排序和动态规划的组合,我们能够高效地解决这个问题。尽管最坏情况下时间复杂度是 (O(n^2)),但对于大多数常见的输入规模,这种方法已经足够高效。