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

232 阅读5分钟

题目

小E的倍数关系子集问题 (题目链接)

题目内容

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

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

由于结果可能非常大,输出的结果需要对 109+710^9+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. n:表示集合的元素数量。
  2. a:一个长度为 n 的数组,表示集合中的元素。

而我们要做的是,找出所有可能的子集,子集是互相为倍数的(这里的子集中,任意两两的元素都有倍数关系。

比如样例3:

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

这里的所有满足条件的子集是【3, 6】、【3, 9】、【3, 12】、【6, 12】、【3,6,12】 所以一共是5个,输出5

这里需要注意的是单个元素的子集不要计算在内(但是对我们后续计算别的有帮助

像这种子集相关的问题,答案与答案之间是有重合的,比如【3, 6】和【3,6,12】,这个就是后者包含了前者。这意味着什么呢?意味着暗示我们需要逐步地从小规模的子集开始构建,并且对子集的构建进行记录和扩展

而算法思想中的动态规划就非常适合这种场景!遇到这种答案与答案之间有重合一部分的关系,比如这种子集的问题,我们都可以优先考虑动态规划!动态规划非常适合这种有重叠子问题的场景。因为我们可以通过构建较小的解并将其存储(记忆化),以用于后续更大的解。 构建一个一维数组dp[],或者二维数组dp[],然后开始动态规划的算法设计。

还有一个需要注意的是,动态规划用的数据应该都是有序的,我们得先排好序那个数组再做后续的处理,这个别忘了(主要是为了方便赋予dp数组答案上的意义)。

动态规划主要有三个步骤:

1.确定dp的纬度(一般是小于等于二维)

2.确定dp的初始状态

3.确定dp的状态转移方程

那么开始这三个步骤

1.确定dp的纬度(一般是小于等于二维)

让我们从一维数组开始考虑吧,如果是dp[]的话,应该怎么赋予dp[i]的意义呢?我们要把这个和答案联系起来,就把每一个dp[i]看成一个答案吧——表示以元素 a[i] 为最大元素的倍数关系子集的个数

如果某个元素 a[i] 是某个较小元素 a[j] 的倍数,那么以 a[j] 结尾的子集,可以通过添加 a[i] 来形成更大的子集。

这意味着在构建以 a[i] 为最大元素的子集时,可以利用已经构建好的以 a[j] 为最大元素的子集。

2.确定dp的初始状态

数组默认为0,很多情况dp的初始元素也确实为0,但是也有部分地方需要设置初始参数.很多类似的动态规划初始设置为0之外,还有很多情况需要设置为1(不然状态转移方程经常用到的dp加法式子怎么能起效呢)。

初始时,要让dp[i] = 1,因为每个元素至少可以形成一个包含它自身的单个元素的子集。虽然这些单个元素的子集不符合最终要求!但它们对于递归地构建更大的子集是有帮助的。

3.确定dp的状态转移方程

记住,由第1步骤我们已经知道这个dp[i]这个对应答案子集的最大倍数是a[i],此时a[i]是顶层。

对于每个元素 a[i],我们遍历所有比 a[i] 小的元素 a[j],如果 a[i]a[j] 的倍数(即 a[i] % a[j] == 0),那么我们可以通过将 a[i] 添加到以 a[j] 为最大值的子集上来构成一个新的子集。

  1. 计算结果:最终,我们累加所有子集数量,去除只有单个元素的子集,并对 109+710^9+7 取模。

代码

import java.util.*;

public class Main {
    public static int solution(int n, int[] a) {
        final int MOD = 1000000007;
        Arrays.sort(a);
        long[] dp = new long[n];
        long result = 0;

        // 初始化动态规划数组
        for (int i = 0; i < n; i++) {
            dp[i] = 1; // 每个元素自己可以是一个子集
        }

        // 构建倍数关系并更新dp数组
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < i; j++) {
                if (a[i] % a[j] == 0) {
                    dp[i] = (dp[i] + dp[j]) % MOD;
                }
            }
        }

        // 计算最终结果
        for (int i = 0; i < n; i++) {
            result = (result + dp[i]) % MOD;
        }

        // 减去所有单个元素子集(n个)
        result = (result - n + MOD) % MOD;

        return (int) result;
    }

    public static void main(String[] args) {
        System.out.println(solution(5, new int[]{1, 2, 3, 4, 5}) == 6);
        System.out.println(solution(6, new int[]{2, 4, 8, 16, 32, 64}) == 57);
        System.out.println(solution(4, new int[]{3, 6, 9, 12}) == 5);
    }
}

复杂度分析

  • 时间复杂度:排序的时间复杂度为 O(nlogn)O(n \log n),而双重循环用于计算倍数关系的复杂度为 O(n2)O(n^2)。总体复杂度为 O(n2)O(n^2),在 n 较大时可能会有性能问题,但对于一般情况下的输入规模是可以接受的。
  • 空间复杂度:我们使用了长度为 n 的数组 dp 来存储中间状态,因此空间复杂度为 O(n)O(n)

总结

省流:排序,注意每次涉及数值运算要取模%MOD,一维数组dp[],初始化值dp[i] = 1,dp[i] = (dp[i] + dp[j]) % MOD。