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

58 阅读3分钟

问题描述

小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

题目分析

本题是一个较为经典的方案计数问题,如果我们采用暴力枚举集合的方式去完成这道题目,时间复杂度将会是恐怖的O(2n)O(2^n),很明显是不优的做法,既然如此,我们就要尝试观察题目性质,采用动态规划的方式去解决问题。

本题题目要求子集内的所有元素两两之间互为倍数关系。需要检查倍数关系,首先对数组进行排序是有益的,通过排序,可以确保在检查倍数关系时,前面的元素一定是较小的。

动态规划-状态设置

我们采用类似于LIS问题(最长上升子序列)问题的解决方案,dp[i]表示排序后,以a[i]为最大元素的集合方案数为多少。

动态规划-转移方式
  1. 初始化动态规划数组:创建一个动态规划数组 dp,初始化所有元素为 1 ,即 a[i] 独立作为一个集合的情况。

  2. 动态规划更新:遍历数组中的每个元素 a[i],对于每个 a[i],再遍历其之前的所有元素 a[j]

    • 如果 a[i] 是 a[j] 的倍数,则 dp[i] 增加 dp[j] 的值。
  3. 最终结果是所有 dp[i] - 1 的和,因为每个子集至少包含两个元素,所以我们需要减去最开始设置的每个 a[i] 独立作为一个集合的情况。

时空复杂度

时间复杂度O(n2)O(n^2),空间复杂度O(n)O(n)

算法步骤

  1. 排序,保证前面的元素一定是较小的。
  2. 创建一个动态规划数组 dp,初始化为 1。
  3. 遍历数组中的每个元素 a[i],对于每个 a[i],再遍历其之前的所有元素 a[j]。如果 a[i] 是 a[j] 的倍数,则 dp[i] 增加 dp[j] 的值。
  4. 最终结果是所有 dp[i] - 1 的和,因为每个子集至少包含两个元素。
  5. 在每次更新 dp[i] 和 ans 时,都进行取模操作,以防止溢出。

代码 - C++

#include <algorithm>
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int mod = 1e9+7;
int solution(int n, vector<int> a) {
    sort(a.begin(),a.end());
    ll ans = 0;
    vector<ll> dp(n,0);
    for(int i=0;i<n;i++) {
        if(a[i] <= 0) continue;
        dp[i] = 1;
        for(int j=0;j<i;j++) {
            if(a[j] <= 0) continue;;
            if(a[i] % a[j] == 0) dp[i] += dp[j];
        }
        dp[i] %= mod;
        ans += dp[i] - 1;
    }
    return (ans % mod + mod) % mod;
}

思考

本题的思考是从排序出发,抓住了题目中最关键的倍数性质,通过排序的方式大大降低了题目思考的难度,并套用了LIS问题的解决方式设置状态并递推。

在解决复杂问题时,可以尝试将问题抽象化,找到通用的解法,再具体问题具体分析,将通用的解决方式与经验套用到不同的场景之中。