问题描述
小E想知道一个给定集合中,有多少个子集满足以下条件:
- 子集内的所有元素数量大于 1。
- 子集内的所有元素两两之间互为倍数关系。
由于结果可能非常大,输出的结果需要对 取模。
样例
样例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
题目分析
本题是一个较为经典的方案计数问题,如果我们采用暴力枚举集合的方式去完成这道题目,时间复杂度将会是恐怖的,很明显是不优的做法,既然如此,我们就要尝试观察题目性质,采用动态规划的方式去解决问题。
本题题目要求子集内的所有元素两两之间互为倍数关系。需要检查倍数关系,首先对数组进行排序是有益的,通过排序,可以确保在检查倍数关系时,前面的元素一定是较小的。
动态规划-状态设置
我们采用类似于LIS问题(最长上升子序列)问题的解决方案,dp[i]表示排序后,以a[i]为最大元素的集合方案数为多少。
动态规划-转移方式
-
初始化动态规划数组:创建一个动态规划数组
dp,初始化所有元素为 1 ,即 a[i] 独立作为一个集合的情况。 -
动态规划更新:遍历数组中的每个元素
a[i],对于每个a[i],再遍历其之前的所有元素a[j]。- 如果
a[i]是a[j]的倍数,则dp[i]增加dp[j]的值。
- 如果
-
最终结果是所有
dp[i] - 1的和,因为每个子集至少包含两个元素,所以我们需要减去最开始设置的每个 a[i] 独立作为一个集合的情况。
时空复杂度
时间复杂度,空间复杂度
算法步骤
- 排序,保证前面的元素一定是较小的。
- 创建一个动态规划数组
dp,初始化为 1。 - 遍历数组中的每个元素
a[i],对于每个a[i],再遍历其之前的所有元素a[j]。如果a[i]是a[j]的倍数,则dp[i]增加dp[j]的值。 - 最终结果是所有
dp[i] - 1的和,因为每个子集至少包含两个元素。 - 在每次更新
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问题的解决方式设置状态并递推。
在解决复杂问题时,可以尝试将问题抽象化,找到通用的解法,再具体问题具体分析,将通用的解决方式与经验套用到不同的场景之中。