题目:https://leetcode-cn.com/problems/the-number-of-good-subsets/
状态转移DP
DP:dp[i][state]
好子集的数目
- i: [2, i]范围内的数,i在30以内
- state:10个质数的选中情况
逻辑:
- 每个好子集里,每个质数最多出现一次
- DP基于【对于10个质数,各有 选 or 不选 的情况】
- 对于每个数字,确定有唯一的state
- PD基于【对于每个元素,有 选 or 不选 的情况】
- 选择元素的条件:state不能与i-1时の,同时为1
编程技巧:
- 遍历顺序:不按照数组顺序,按照自然顺序再剪枝。
【数字大小范围限定在30以内】【求总数,与顺序无关】
剪枝1:freq数组记录某个数字的出现次数,不涉及的用0来continue
剪枝2:元素 % 质数的平方 == 0
时,不符题意,滤去
//官方题解
class Solution {
static final int[] PRIMES = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
static final int NUM_MAX = 30;
static final int MOD = 1000000007;
public int numberOfGoodSubsets(int[] nums) {
//遍历一遍计算数字的出现次数
int[] freq = new int[NUM_MAX + 1];
for (int num : nums) {
++freq[num];
}
int[] f = new int[1 << PRIMES.length];
f[0] = 1;
//讨论数字1,锦上添花的作用,选择了则多一倍数目
for (int i = 0; i < freq[1]; ++i) {
f[0] = f[0] * 2 % MOD;
}
//对于每个数字范围(从2开始)
for (int i = 2; i <= NUM_MAX; ++i) {
if (freq[i] == 0) {
continue;
}
// 检查 i 的每个质因数是否均不超过 1 个
int subset = 0, x = i;
boolean check = true;
//对于每个质数
for (int j = 0; j < PRIMES.length; ++j) {
int prime = PRIMES[j];
//【质数的平方】为因数,则滤去
if (x % (prime * prime) == 0) {
check = false;
break;
}
//数字为质数,则得到其自身的使用情况二进制编码
//【巧妙】有没有可能 x = 质数1 * 质数2 = 质数3 * 质数4 ? 不可能!
if (x % prime == 0) {
subset |= (1 << j);
}
}
//数字为质数的平方则滤去
if (!check) {
continue;
}
// 动态规划
for (int mask = (1 << PRIMES.length) - 1; mask > 0; --mask) {
//如果mask包含(i唯一对应的)subset
if ((mask & subset) == subset) {
//编程技巧:倒序遍历(大mask需要用到上一轮的小mask)
f[mask] = (int) ((f[mask] + ((long) f[mask ^ subset]) * freq[i]) % MOD);
}
}
} //对于每个数字范围
int ans = 0;
//不从0开始。因为单独的f[0],分解后无质数,不符题意
for (int mask = 1, maskMax = (1 << PRIMES.length); mask < maskMax; ++mask) {
ans = (ans + f[mask]) % MOD;
}
return ans;
}
}