本题的数据范围比较小决定了可以使用状态压缩dp。
本题要把子集中的所有数的乘积拆成不同的质因子,必须子集中都持有不同的质因子,并且每个元素子集不能持有重复的质因子。
由于数据范围是1~30,所以总共只有10个质数,我们可以用一个长度为10的二进制串表示我们子集中每个数持有的质因子的情况。
然后考虑定义状态为
f
(
i
,
m
a
s
k
)
f(i, mask)
f(i,mask)表示考虑2~i的数,子集的质因子分布情况是
m
a
s
k
mask
mask的总子集方案数。
若当前遍历的数i不存在于数组中,则没法转移这个数;
若当前遍历的数i持有重复的质因子,则没法选当前数入子集,状态转移方程为
f
[
i
]
[
m
a
s
k
]
=
f
[
i
−
1
]
[
m
a
s
k
]
f[i][mask] = f[i - 1][mask]
f[i][mask]=f[i−1][mask];
若当前遍历的数不持有重复的质因子,设当前数质因子的持有情况是
s
u
b
s
e
t
subset
subset,则可以选当前数入子集,有
c
n
t
[
i
]
cnt[i]
cnt[i]个当前数i,则有
c
n
t
[
i
]
cnt[i]
cnt[i]种选择,也可以不选,状态转移方程为:
f
[
i
]
[
m
a
s
k
]
=
f
[
i
−
1
]
[
m
a
s
k
]
+
c
n
t
[
i
]
∗
f
[
i
−
1
]
[
m
a
s
k
s
u
b
s
e
t
]
,
i
f
(
(
m
a
s
k
&
s
u
b
s
e
t
)
=
=
s
u
b
s
e
t
)
f[i][mask] = f[i - 1][mask] + cnt[i] * f[i - 1][mask subset],if ((mask \& subset) == subset)
f[i][mask]=f[i−1][mask]+cnt[i]∗f[i−1][masksubset],if((mask&subset)==subset)
这里利用了异或可以把相同的位消掉,不同的位保留为1的性质,并且只有当前考虑的质因子分布中包含了i的质因子分布才行,对应到二进制位上,
s
u
b
s
e
t
subset
subset必须是
m
a
s
k
mask
mask的子集。
class Solution {
public:
int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
static const int N = 1e9 + 7;
int numberOfGoodSubsets(vector<int>& nums)
{
// 先看数据范围 1 <= nums[i] <= 30 在这 1~30中 只有这些质数:
// 2 3 5 7 11 13 17 19 23 29 总共10个
// 本题要求把乘积拆成一个或多个互不相同的质因子的乘积
// 我们可以用一个10位的二进制数字mask来表示每个质因子是否已经被选过了
// 定义f[i][mask]表示在质因子选择情况是mask的情况下 考虑2~i范围内的数一共有多少种方案
// 答案就是求和f[30][k] k 从1到1 << 10 表示所有质数选择情况
// 状态转移方程:若i含有多个相同的质因子 则不能选i 方程为f[i][mask] = f[i - 1][mask]
// 若i含有多个不同的质因子 记它们的质因子分布情况是subset(一个10位的二进制数字)
// 若subset是mask的子集,即mask & subset == subset, 这样才能转移
// 状态转移方程就为 f[i][mask] = f[i - 1][mask] + f[i - 1][mask ^ subset] * cnt[i]
// 可以选cnt[i]个i,所以还要乘上cnt[i]
// 异或使相同的全变0,不相同的全变1
// 初始条件f[1][0] = 2^(cnt[1]),因为每个1都可以选或不选,增加2^(cnt[1])个情况
vector<int> cnt(31);
// 计数
for (auto l : nums)
{
++cnt[l];
}
int mask_max = 1 << 10;
// 状态转移的数组 考虑到仅和前继有关 所以用一个一维数组优化
vector<int> dp(mask_max);
// 初始条件
dp[0] = 1;// 先初始化成1 这里的dp[0]就是f[1][0]
// 不管有没有1 我们都可以假设有1个 这样不影响乘法计数
// 然后把dp[0]维护为2^(cnt[1])
for (int i = 0; i < cnt[1]; ++i)
{
dp[0] = dp[0] * 2 % N;
}
for (int i = 2; i <= 30; ++i)
{
// 若数组中没这个元素 也没必要选了
if (!cnt[i]) continue;
// 看看它是否持有重复的质因子 同时统计它质因子出现的情况
int x = i, subset = 0;
bool flag = true;
for (int j = 0; j < 10; ++j)
{
if (x % (prime[j] * prime[j]) == 0)
{
flag = false;
break;
}
if (x % prime[j] == 0)
{
subset |= (1 << j);
}
}
// 如果持有重复质因子 就不考虑这个数了
if (flag == false)
{
continue;
}
// 否则进行动态规划
// 由于subset是mask的子集 所以subset ^ mask一定比 mask小
// 所以一维数组的动态规划优化为了保证计算到dp[mask ^ subset]
// 还是上一层的f[i - 1][mask ^ subset] 所以循环从mask_max - 1往小走
for (int mask = mask_max - 1; mask > 0; --mask)
{
if ((subset & mask) == subset)
{
dp[mask] =
(dp[mask] + static_cast<long long>(dp[mask ^ subset]) * cnt[i]) % N;
}
}
}
int ret = 0;
for (int mask = 1; mask < mask_max; ++mask)
{
ret = (ret + dp[mask]) % N;
}
return ret;
}
};
本题还可以剪枝,本题中 子集中不可能插入
4
、
8
、
9
、
12
、
16
、
18
、
20
、
24
、
25
、
27
、
28
4、8、9、12、 16、 18、 20、 24、 25、 27、 28
4、8、9、12、16、18、20、24、25、27、28这些元素,因为他们都含有质数的平方
2
∗
2
2*2
2∗2或者
3
∗
3
3*3
3∗3 或者
5
∗
5
5*5
5∗5,利用一个哈希表提前存好这些数字,若hash.count(i) != 0
直接continue即可。
class Solution {
public:
int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
int mask_max = (1 << 10) - 1;
unordered_set<int> hash = {4, 8, 9, 12, 16, 18, 20, 24, 25, 27, 28};
static const int N = 1e9 + 7;
int numberOfGoodSubsets(vector<int>& nums)
{
vector<int> cnt(31);
for (int num : nums){
++cnt[num];
}
vector<int> dp(mask_max + 1);
dp[0] = 1;
for (int i = 0; i < cnt[1]; ++i)
{
dp[0] = dp[0] * 2 % N;
}
for (int i = 2; i <= 30; ++i)
{
if (cnt[i] == 0 || hash.count(i) != 0) continue;
int x = i, subset = 0;
bool flag = true;
for (int j = 0; j < 10; ++j)
{
if (x % (prime[j] * prime[j]) == 0){
flag = false;
break;
}
if ((x % prime[j]) == 0)
{
subset |= (1 << j);
}
}
if (flag == false) continue;
for (int mask = mask_max; mask > 0; --mask)
{
if ((mask & subset) == subset)
{
dp[mask] =
(dp[mask] + static_cast<long long>(dp[mask ^ subset]) * cnt[i]) % N;
}
}
}
int ret = 0;
for (int mask = 1; mask <= mask_max; ++mask)
{
ret = (ret + dp[mask]) % N;
}
return ret;
}
};