解题笔记:小E的倍数关系子集问题
问题描述
给定一个数组 a,我们需要找到其中所有满足以下条件的子集:
- 子集内的元素数量大于 1。
- 子集内的所有元素两两之间互为倍数关系。
由于子集的数量可能非常大,要求输出结果对 109+710^9 + 7 取模。
问题分析
本题的核心是通过动态规划来计算每个元素作为最大值时,能构成多少个合法的倍数关系子集。动态规划数组 dp[i] 表示以 a[i] 为最大值的合法子集数量。
- 排序:首先对数组进行排序,便于后续检查倍数关系。
- 动态规划:定义一个数组
dp[i],表示以a[i]为最大值的子集数量。初始化时,每个元素可以单独构成一个子集,因此dp[i] = 1。 - 倍数关系判断:对于每一个元素
a[i],我们遍历其前面所有的元素a[j],如果a[i] % a[j] == 0,则a[i]可以和a[j]构成一个倍数关系的子集。 - 排除单元素子集:最终的合法子集数量中需要排除掉单元素子集。
解题思路
- 排序:我们首先将数组排序,保证了倍数关系的检查顺序。这样,检查一个元素
a[i]时,只需要检查a[j](j < i)是否可以作为其倍数。 - 动态规划:通过动态规划数组
dp[i]来存储以a[i]为最大值的合法子集数量。对于每个元素,遍历其前面的所有元素,判断是否能构成倍数关系,如果能,则将前面元素的子集数量累加到当前元素的子集数量中。 - 结果计算:计算所有子集的数量,减去单元素子集的数量,得到符合条件的子集数量。
代码解析
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)来求解满足倍数关系的子集数量。具体步骤如下:
- 对数组进行排序,保证倍数关系检查的顺序。
- 初始化一个动态规划数组
dp,记录以每个元素为最大值的合法子集数量。 - 对每个元素,遍历它之前的所有元素,检查倍数关系,如果成立,则更新
dp数组。 - 最终通过对
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),适用于中等规模的输入数据,但对于超大规模数据可能会遇到性能瓶颈。尽管如此,该方法依然是一种简洁有效的解决方案,能够在多数情况下提供较好的性能。