无平方子集计数
题目描述
给你一个正整数数组 nums 。
如果数组 nums 的子集中的元素乘积是一个 无平方因子数 ,则认为该子集是一个 无平方 子集。
无平方因子数 是无法被除 1 之外任何平方数整除的数字。
返回数组 nums 中 无平方 且 非空 的子集数目。因为答案可能很大,返回对 109 + 7 取余的结果。
nums 的 非空子集 是可以由删除 nums 中一些元素(可以不删除,但不能全部删除)得到的一个数组。如果构成两个子集时选择删除的下标不同,则认为这两个子集不同。
样例
样例输入
nums = [3,4,4,5]
nums = [1]
样例输出
3
解释:示例中有 3 个无平方子集:
由第 0 个元素 [3] 组成的子集。其元素的乘积是 3 ,这是一个无平方因子数。
由第 3 个元素 [5] 组成的子集。其元素的乘积是 5 ,这是一个无平方因子数。
由第 0 个和第 3 个元素 [3,5] 组成的子集。其元素的乘积是 15 ,这是一个无平方因子数。
可以证明给定数组中不存在超过 3 个无平方子集。
1
解释:示例中有 1 个无平方子集:
由第 0 个元素 [1] 组成的子集。其元素的乘积是 1 ,这是一个无平方因子数。
可以证明给定数组中不存在超过 1 个无平方子集。
提示
- 1 <= nums.length <= 1000
- 1 <= nums[i] <= 30
思路
题目的需求还是很清晰,需要求出所有区间内所有质因子不相同的区间数量,但题中有要求要对1e9 + 7求余,那么每次都遍历当前区间的质因子个数,就不现实。需要很清晰的表达出当前数或当前区间的所有质因子,周赛的时候我是没写出来,对于我来说还是难,是需要特地学的程度。
状态压缩dp,很难,但要是能自己写出来,感觉就能非常厉害。
题目的本质是有多少个质因子不相同的和。需要很方便的表示所有数的质因子,可以使用二进制来例举(巨厉害的想法)。
代码实现
01背包类解法
class Solution {
public int squareFreeSubsets(int[] nums) {
int[] assist = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
int n = nums.length;
int[] arr = new int[31];
for(int i = 2; i < 31; i++){
for(int j = 0; j < assist.length; j++){
if(i % assist[j] == 0){
if(i % (assist[j] * assist[j]) == 0){
arr[i] = -1;
break;
}
arr[i] |= 1 << j;
}
}
}
// 以上都是把数据进行预处理
// 转化为相对应的二进制形式
int MOD = (int)1e9 + 7;
int m = 1 << assist.length;
int[] dp = new int[m];
dp[0] = 1;
for(int num : nums){
int mask = arr[num];
if(mask >= 0){
for(int j = m-1; j >= mask; j--){
if((mask | j) == j)
dp[j] = (dp[j] + dp[j ^ mask]) % MOD;
}
}
}
var ans = 0L;
for(int d : dp) ans += d;
// 减1是需要减去dp的边界条件,如果dp[0]不初始化为1,那么dp数组全为0.
return (int)((ans - 1) % MOD);
}
}
状压DP(暴力版)
class Solution {
static final int[] assist = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
static final int MOD = (int)1e9 + 7, max = 30, n = assist.length, m = 1 << n;
static final int[] arr = new int[max+1];
static{
for(int i = 2; i <= max; i++){
for(int j = 0; j < n; j++){
if(i % (assist[j]) == 0){
if(i % (assist[j] * assist[j]) == 0){
arr[i] = -1;
break;
}
arr[i] |= 1 << j;
}
}
}
}
// 以上都是数组的预处理
public int squareFreeSubsets(int[] nums) {
int n = nums.length;
var cnt = new int[max + 1];
int pow2 = 1;
for(int x : nums){
if(x == 1) pow2 = pow2 * 2 % MOD;
else cnt[x]++;
}
var dp = new long[m];
dp[0] = 1;
for(int i = 2; i < 31; i++){
int mask = arr[i], c = cnt[i];
if(mask >= 0 && c > 0){
// 直接枚举所有大于mask的情况,但只有mask & k == 0的情况才会进入状态转移。
// 时间上还能进行优化。
for(int k = m - 1; k >= 0; k--){
if((mask & k) == 0){
dp[mask | k] = (dp[mask | k] + dp[k] * c) % MOD;
}
}
}
}
var ans = 0L;
for(var c : dp) ans += c;
return (int)((ans % MOD * pow2 - 1) % MOD);
}
}
状压DP(最优版)
class Solution {
static final int[] assist = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
static final int MOD = (int)1e9 + 7, max = 30, n = assist.length, m = 1 << n;
static final int[] arr = new int[max+1];
static{
for(int i = 2; i <= max; i++){
for(int j = 0; j < n; j++){
if(i % (assist[j]) == 0){
if(i % (assist[j] * assist[j]) == 0){
arr[i] = -1;
break;
}
arr[i] |= 1 << j;
}
}
}
}
public int squareFreeSubsets(int[] nums) {
int n = nums.length;
var cnt = new int[max + 1];
int pow2 = 1;
for(int x : nums){
if(x == 1) pow2 = pow2 * 2 % MOD;
else cnt[x]++;
}
var dp = new long[m];
dp[0] = 1;
for(int i = 2; i < 31; i++){
int mask = arr[i], c = cnt[i];
if(mask >= 0 && c > 0){
int other = (m-1) ^ mask, j = other;
while(true){
dp[j | mask] = (dp[j | mask] + dp[j] * c) % MOD;
// (j -1) & other = j -= j&(-j)结果差不多,都是减去最后一位1的方式。
// 因为还需要枚举全0的情况,所以没用j > 0的情况控制循环
// 因为-1的二进制形式为全1,所以最后j = 0时,还会进入一次循环,并且与and -1后等于自己
// 所以可以使用 j == other 最为跳出循环的条件。
// 当然使用do while(j != other) 控制循环也可。
j = (j - 1) & other;
if(j == other) break;
}
}
}
var ans = 0L;
for(var c : dp) ans += c;
return (int)((ans % MOD * pow2 - 1) % MOD);
}
}