小E的倍数关系子集问题 | 豆包MarsCode AI 刷题

110 阅读6分钟

解题笔记:小E的倍数关系子集问题

问题描述

给定一个数组 a,我们需要找到其中所有满足以下条件的子集:

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

由于子集的数量可能非常大,要求输出结果对 109+710^9 + 7 取模。

问题分析

本题的核心是通过动态规划来计算每个元素作为最大值时,能构成多少个合法的倍数关系子集。动态规划数组 dp[i] 表示以 a[i] 为最大值的合法子集数量。

  1. 排序:首先对数组进行排序,便于后续检查倍数关系。
  2. 动态规划:定义一个数组 dp[i],表示以 a[i] 为最大值的子集数量。初始化时,每个元素可以单独构成一个子集,因此 dp[i] = 1
  3. 倍数关系判断:对于每一个元素 a[i],我们遍历其前面所有的元素 a[j],如果 a[i] % a[j] == 0,则 a[i] 可以和 a[j] 构成一个倍数关系的子集。
  4. 排除单元素子集:最终的合法子集数量中需要排除掉单元素子集。

解题思路

  1. 排序:我们首先将数组排序,保证了倍数关系的检查顺序。这样,检查一个元素 a[i] 时,只需要检查 a[j]j < i)是否可以作为其倍数。
  2. 动态规划:通过动态规划数组 dp[i] 来存储以 a[i] 为最大值的合法子集数量。对于每个元素,遍历其前面的所有元素,判断是否能构成倍数关系,如果能,则将前面元素的子集数量累加到当前元素的子集数量中。
  3. 结果计算:计算所有子集的数量,减去单元素子集的数量,得到符合条件的子集数量。

代码解析

def solution(n: int, a: list[int]) -> int:
    """
    计算满足条件的子集数量,结果对 10^9 + 7 取模。

    输入:
        n: int - 数组长度
        a: list[int] - 元素数组
    输出:
        int - 满足条件的子集数量
    """
    MOD = 10**9 + 7  # 模数
    a.sort()  # 对数组进行排序
    dp = [0] * n  # dp 数组初始化,大小为 n
步骤 1:初始化常量和数据结构
  • MOD = 10^9 + 7:定义了取模常量,避免结果超出范围。
  • a.sort() :对数组 a 进行升序排序。排序是为了使得在检查倍数关系时,我们可以仅仅考虑当前元素前面已经遍历的元素。
  • dp = [0] * n:定义一个动态规划数组 dp,其中 dp[i] 表示以 a[i] 为最大值的合法子集数量。

    for i in range(n):
        dp[i] = 1  # 初始化,每个元素单独构成一个子集
步骤 2:初始化每个元素的子集
  • 对于每一个元素 a[i],我们假设它自身就可以构成一个合法的子集,因此初始化 dp[i] = 1

    for i in range(n):
        for j in range(i):
            if a[i] % a[j] == 0:  # 判断 a[i] 是否是 a[j] 的倍数
                dp[i] += dp[j]  # 将以 a[j] 为最大值的子集数量加到 a[i] 的子集中
                dp[i] %= MOD  # 对每次更新都取模,避免溢出
步骤 3:检查倍数关系并更新子集数量
  • 外层循环 for i in range(n) 遍历每个元素。
  • 内层循环 for j in range(i) 遍历 a[i] 之前的元素。
  • 倍数判断:通过 a[i] % a[j] == 0 判断 a[i] 是否是 a[j] 的倍数。如果成立,则说明 a[i] 可以与 a[j] 形成一个合法的倍数关系子集。
  • 更新 dp[i] :如果 a[i] 可以与 a[j] 形成合法子集,那么将 dp[j] 的值加到 dp[i] 中。这表示所有以 a[j] 为最大值的子集都可以扩展为以 a[i] 为最大值的子集。
  • 取模:每次更新 dp[i] 后,使用 dp[i] %= MOD 取模,防止数据溢出。

    total = sum(dp) % MOD  # dp 数组中的值是所有子集的数量
    result = (total - n) % MOD  # 减去所有单元素子集
    return result if result >= 0 else (result + MOD)  # 处理可能的负数结果
步骤 4:计算最终结果
  • total = sum(dp) % MOD:计算所有子集的数量。dp[i] 中存储的是以 a[i] 为最大值的合法子集数量,所以 sum(dp) 就是所有合法子集的总数。
  • result = (total - n) % MOD:我们需要减去单元素子集的数量 n,因为题目要求子集内的元素数量大于 1。
  • return result if result >= 0 else (result + MOD) :返回最终结果。如果 result 为负数,则通过 (result + MOD) 处理负数的情况,确保返回非负数。

代码总结

该算法利用动态规划(DP)来求解满足倍数关系的子集数量。具体步骤如下:

  1. 对数组进行排序,保证倍数关系检查的顺序。
  2. 初始化一个动态规划数组 dp,记录以每个元素为最大值的合法子集数量。
  3. 对每个元素,遍历它之前的所有元素,检查倍数关系,如果成立,则更新 dp 数组。
  4. 最终通过对 dp 数组求和,减去单元素子集数量,得到符合条件的子集总数。

该算法的时间复杂度为 O(n2)O(n^2),适用于中等规模的输入。

测试用例说明

print(solution(5, [1, 2, 3, 4, 5]))  # 输出: 6
print(solution(6, [2, 4, 8, 16, 32, 64]))  # 输出: 57
print(solution(4, [3, 6, 9, 12]))  # 输出: 5
  • 测试用例1:对于数组 [1, 2, 3, 4, 5],共有 6 个满足条件的子集。
  • 测试用例2:对于数组 [2, 4, 8, 16, 32, 64],共有 57 个符合条件的子集。
  • 测试用例3:对于数组 [3, 6, 9, 12],共有 5 个符合条件的子集。

总结

本题通过动态规划(DP)成功解决了倍数关系子集的计数问题。通过定义一个 dp 数组,其中 dp[i] 记录以 a[i] 为最大值的合法子集数量,结合倍数关系的检查,我们能够高效地计算出符合条件的子集数。排序是关键步骤,它确保我们能够在检查倍数关系时,仅仅考虑当前元素之前的元素,从而减少不必要的计算。通过动态规划递推,我们逐步累加每个元素能构成的合法子集的数量。

最终,我们通过对所有合法子集数量求和,并减去单元素子集的数量,得到了题目要求的结果。取模操作则保证了结果在不溢出的范围内。整个解法的时间复杂度为 O(n2)O(n^2),适用于中等规模的输入数据,但对于超大规模数据可能会遇到性能瓶颈。尽管如此,该方法依然是一种简洁有效的解决方案,能够在多数情况下提供较好的性能。