2572-无平方子集计数

Raphael Liu Lv10

给你一个正整数数组 nums

如果数组 nums 的子集中的元素乘积是一个 无平方因子数 ,则认为该子集是一个 无平方 子集。

无平方因子数 是无法被除 1 之外任何平方数整除的数字。

返回数组 nums无平方非空 的子集数目。因为答案可能很大,返回对 109 + 7 取余的结果。

nums非空子集 是可以由删除 nums
中一些元素(可以不删除,但不能全部删除)得到的一个数组。如果构成两个子集时选择删除的下标不同,则认为这两个子集不同。

示例 1:

**输入:** nums = [3,4,4,5]
**输出:** 3
**解释:** 示例中有 3 个无平方子集:
- 由第 0 个元素 [3] 组成的子集。其元素的乘积是 3 ,这是一个无平方因子数。
- 由第 3 个元素 [5] 组成的子集。其元素的乘积是 5 ,这是一个无平方因子数。
- 由第 0 个和第 3 个元素 [3,5] 组成的子集。其元素的乘积是 15 ,这是一个无平方因子数。
可以证明给定数组中不存在超过 3 个无平方子集。

示例 2:

**输入:** nums = [1]
**输出:** 1
**解释:** 示例中有 1 个无平方子集:
- 由第 0 个元素 [1] 组成的子集。其元素的乘积是 1 ,这是一个无平方因子数。
可以证明给定数组中不存在超过 1 个无平方子集。

提示:

  • 1 <= nums.length <= 1000
  • 1 <= nums[i] <= 30

方法一:转换成 0-1 背包方案数

把无平方因子数的数字记作 SF(square-free number)。

对于每个 [2,30] 内的 SF,通过预处理得到每个 SF 的质因子集合,用二进制表示。二进制从低到高第 i 个比特为 1 表示第 i 个质数在集合中,为 0 表示第 i 个质数不在集合中。

那么把每个是 SF 的 nums}[i] 转换成对应的质因子集合,题目就变成「遍历所有由 30 以内的质数组成的集合 j(这有 2^{10 个),对每个 j,计算选一些不相交的质因子集合,它们的恰好为 j 的方案数」。

这是 0-1 背包求方案数的模型,具体可以看【基础算法精讲 18】

附:本题视频讲解

[sol1-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
PRIMES = 2, 3, 5, 7, 11, 13, 17, 19, 23, 29
SF_TO_MASK = [0] * 31 # SF_TO_MASK[i] 为 i 的质因子集合(用二进制表示)
for i in range(2, 31):
for j, p in enumerate(PRIMES):
if i % p == 0:
if i % (p * p) == 0: # 有平方因子
SF_TO_MASK[i] = -1
break
SF_TO_MASK[i] |= 1 << j # 把 j 加到集合中

class Solution:
def squareFreeSubsets(self, nums: List[int]) -> int:
MOD = 10 ** 9 + 7
M = 1 << len(PRIMES)
f = [0] * M # f[j] 表示恰好组成质数集合 j 的方案数
f[0] = 1 # 质数集合是空集的方案数为 1
for x in nums:
mask = SF_TO_MASK[x]
if mask >= 0: # x 是 SF
for j in range(M - 1, mask - 1, -1):
if (j | mask) == j: # mask 是 j 的子集
f[j] = (f[j] + f[j ^ mask]) % MOD # 不选 mask + 选 mask
return (sum(f) - 1) % MOD # -1 去掉空集(nums 的空子集)
[sol1-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Solution {
private static final int[] PRIMES = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
private static final int MOD = (int) 1e9 + 7, MX = 30, N_PRIMES = PRIMES.length, M = 1 << N_PRIMES;
private static final int[] SF_TO_MASK = new int[MX + 1]; // SF_TO_MASK[i] 为 i 的质因子集合(用二进制表示)

static {
for (int i = 2; i <= MX; ++i)
for (int j = 0; j < N_PRIMES; ++j) {
int p = PRIMES[j];
if (i % p == 0) {
if (i % (p * p) == 0) { // 有平方因子
SF_TO_MASK[i] = -1;
break;
}
SF_TO_MASK[i] |= 1 << j; // 把 j 加到集合中
}
}
}

public int squareFreeSubsets(int[] nums) {
var f = new int[M]; // f[j] 表示恰好组成质数集合 j 的方案数
f[0] = 1; // 质数集合是空集的方案数为 1
for (int x : nums) {
int mask = SF_TO_MASK[x];
if (mask >= 0) // x 是 SF
for (int j = M - 1; j >= mask; --j)
if ((j | mask) == j) // mask 是 j 的子集
f[j] = (f[j] + f[j ^ mask]) % MOD; // 不选 mask + 选 mask
}
var ans = 0L;
for (int v : f) ans += v;
return (int) ((ans - 1) % MOD); // -1 去掉空集(nums 的空子集)
}
}
[sol1-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class Solution {
static constexpr int PRIMES[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
static constexpr int MOD = 1e9 + 7, MX = 30, N_PRIMES = 10, M = 1 << N_PRIMES;
public:
int squareFreeSubsets(vector<int> &nums) {
int sf2mask[MX + 1]{}; // sf2mask[i] 为 i 的质因子集合(用二进制表示)
for (int i = 2; i <= MX; ++i)
for (int j = 0; j < N_PRIMES; ++j) {
int p = PRIMES[j];
if (i % p == 0) {
if (i % (p * p) == 0) { // 有平方因子
sf2mask[i] = -1;
break;
}
sf2mask[i] |= 1 << j; // 把 j 加到集合中
}
}

int f[M]{1}; // f[j] 表示恰好组成质数集合 j 的方案数,其中质数集合是空集的方案数为 1
for (int x : nums)
if (int mask = sf2mask[x]; mask >= 0) // x 是 SF
for (int j = M - 1; j >= mask; --j)
if ((j | mask) == j) // mask 是 j 的子集
f[j] = (f[j] + f[j ^ mask]) % MOD; // 不选 mask + 选 mask
return (accumulate(f, f + M, 0L) - 1) % MOD; // -1 去掉空集(nums 的空子集)
}
};
[sol1-Go]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
var primes = [...]int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29}
var sf2mask = [31]int{} // sf2mask[i] 为 i 的质因子集合(用二进制表示)

func init() {
for i := 2; i <= 30; i++ {
for j, p := range primes {
if i%p == 0 {
if i%(p*p) == 0 { // 有平方因子
sf2mask[i] = -1
break
}
sf2mask[i] |= 1 << j // 把 j 加到集合中
}
}
}
}

func squareFreeSubsets(nums []int) int {
const mod int = 1e9 + 7
const m = 1 << len(primes)
f := [m]int{1} // f[j] 表示恰好组成质数集合 j 的方案数,其中质数集合是空集的方案数为 1
for _, x := range nums {
if mask := sf2mask[x]; mask >= 0 { // x 是 SF
for j := m - 1; j >= mask; j-- {
if j|mask == j { // mask 是 j 的子集
f[j] = (f[j] + f[j^mask]) % mod // 不选 mask + 选 mask
}
}
}
}
ans := 0
for _, v := range f {
ans += v
}
return (ans - 1) % mod // -1 去掉空集(nums 的空子集)
}

复杂度分析

  • 时间复杂度:O(n2^m),其中 n 为 nums 的长度,m=\pi(\max(\textit{nums})),\pi(N) 表示 N 以内的质数个数,对于本题的数据范围,m\le 10。
  • 空间复杂度:O(2^m)。

方法二:子集状压 DP

如果把 nums 的长度增加到 10^5 的话,方法一就太慢了。

毕竟 nums 的值域很小,把相同数字一并处理是更优的。

怎么转移呢?选择的数字乘积不能有平方因子,对应的质数集合也就不能有交集。那么枚举 mask 的补集 other 的子集 j,这样可以保证 mask 和 j 是没有交集的。站在并集 j\cup \textit{mask 的角度,按照「选或不选」的思想,如果选了 mask,那么就要从 f[j] 转移过来了。

具体地,设 x 是 mask 对应的 SF,cnt}[x] 为 x 在 nums 中的出现次数,那么需要从 cnt}[x] 个 mask 中选一个,与 j 并成 j\cup \textit{mask,对应的转移为

f[j\cup \textit{mask}] += f[j] \cdot \textit{cnt}[x]

附:视频讲解

[sol2-Python3]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
PRIMES = 2, 3, 5, 7, 11, 13, 17, 19, 23, 29
SF_TO_MASK = [0] * 31 # SF_TO_MASK[i] 为 i 的质因子集合(用二进制表示)
for i in range(2, 31):
for j, p in enumerate(PRIMES):
if i % p == 0:
if i % (p * p) == 0: # 有平方因子
SF_TO_MASK[i] = -1
break
SF_TO_MASK[i] |= 1 << j # 把 j 加到集合中

class Solution:
def squareFreeSubsets(self, nums: List[int]) -> int:
MOD = 10 ** 9 + 7
cnt = Counter(nums)
M = 1 << len(PRIMES)
f = [0] * M # f[j] 表示恰好组成质数集合 j 的方案数
f[0] = pow(2, cnt[1], MOD) # 用 1 组成空质数集合的方案数
for x, c in cnt.items():
mask = SF_TO_MASK[x]
if mask > 0: # x 是 SF
j = other = (M - 1) ^ mask # mask 的补集
while True: # 枚举 other 的子集 j
f[j | mask] = (f[j | mask] + f[j] * c) % MOD # 不选 mask + 选 mask
j = (j - 1) & other
if j == other: break
return (sum(f) - 1) % MOD # -1 表示去掉空集(nums 的空子集)
[sol2-Java]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
class Solution {
private static final int[] PRIMES = new int[]{2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
private static final int MOD = (int) 1e9 + 7, MX = 30, N_PRIMES = PRIMES.length, M = 1 << N_PRIMES;
private static final int[] SF_TO_MASK = new int[MX + 1]; // SF_TO_MASK[i] 为 i 的质因子集合(用二进制表示)

static {
for (int i = 2; i <= MX; ++i)
for (int j = 0; j < N_PRIMES; ++j) {
int p = PRIMES[j];
if (i % p == 0) {
if (i % (p * p) == 0) { // 有平方因子
SF_TO_MASK[i] = -1;
break;
}
SF_TO_MASK[i] |= 1 << j; // 把 j 加到集合中
}
}
}

public int squareFreeSubsets(int[] nums) {
var cnt = new int[MX + 1];
int pow2 = 1;
for (int x : nums)
if (x == 1) pow2 = pow2 * 2 % MOD;
else ++cnt[x];

var f = new long[M]; // f[j] 表示恰好组成质数集合 j 的方案数
f[0] = pow2; // 用 1 组成空质数集合的方案数
for (int x = 2; x <= MX; ++x) {
int mask = SF_TO_MASK[x], c = cnt[x];
if (mask > 0 && c > 0) {
int other = (M - 1) ^ mask, j = other; // mask 的补集 other
do { // 枚举 other 的子集 j
f[j | mask] = (f[j | mask] + f[j] * cnt[x]) % MOD; // 不选 mask + 选 mask
j = (j - 1) & other;
} while (j != other);
}
}
var ans = -1L; // 去掉空集(nums 的空子集)
for (var v : f) ans += v;
return (int) (ans % MOD);
}
}
[sol2-C++]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class Solution {
static constexpr int PRIMES[] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
static constexpr int MOD = 1e9 + 7, MX = 30, N_PRIMES = 10, M = 1 << N_PRIMES;
public:
int squareFreeSubsets(vector<int> &nums) {
int sf2mask[MX + 1]{}; // sf2mask[i] 为 i 的质因子集合(用二进制表示)
for (int i = 2; i <= MX; ++i)
for (int j = 0; j < N_PRIMES; ++j) {
int p = PRIMES[j];
if (i % p == 0) {
if (i % (p * p) == 0) { // 有平方因子
sf2mask[i] = 0;
break;
}
sf2mask[i] |= 1 << j; // 把 j 加到集合中
}
}

int cnt[MX + 1]{}, pow2 = 1;
for (int x : nums)
if (x == 1) pow2 = pow2 * 2 % MOD;
else ++cnt[x];

long f[M]{pow2}; // f[j] 表示恰好组成质数集合 j 的方案数,其中用 1 组成空质数集合的方案数为 pow2
for (int x = 2; x <= MX; ++x) {
int mask = sf2mask[x], c = cnt[x];
if (mask && c) {
int other = (M - 1) ^ mask, j = other; // mask 的补集 other
do { // 枚举 other 的子集 j
f[j | mask] = (f[j | mask] + f[j] * cnt[x]) % MOD; // 不选 mask + 选 mask
j = (j - 1) & other;
} while (j != other);
}
}
return accumulate(f, f + M, -1L) % MOD; // -1 表示去掉空集(nums 的空子集)
}
};
[sol2-Go]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
var primes = [...]int{2, 3, 5, 7, 11, 13, 17, 19, 23, 29}
var sf2mask = [31]int{} // sf2mask[i] 为 i 的质因子集合(用二进制表示)

func init() {
for i := 2; i <= 30; i++ {
for j, p := range primes {
if i%p == 0 {
if i%(p*p) == 0 { // 有平方因子
sf2mask[i] = -1
break
}
sf2mask[i] |= 1 << j // 把 j 加到集合中
}
}
}
}

func squareFreeSubsets(a []int) int {
const mod int = 1e9 + 7
cnt, pow2 := [31]int{}, 1
for _, x := range a {
if x == 1 {
pow2 = pow2 * 2 % mod
} else {
cnt[x]++
}
}

const m = 1 << len(primes)
f := [m]int{pow2} // f[j] 表示恰好组成质数集合 j 的方案数,其中用 1 组成空质数集合的方案数为 pow2
for sf, mask := range sf2mask {
if mask > 0 && cnt[sf] > 0 {
other := (m - 1) ^ mask // mask 的补集
for j := other; ; { // 枚举 other 的子集 j
f[j|mask] = (f[j|mask] + f[j]*cnt[sf]) % mod // 不选 mask + 选 mask
j = (j - 1) & other
if j == other {
break
}
}
}
}
ans := -1 // 去掉空集(nums 的空子集)
for _, v := range f {
ans += v
}
return ans % mod
}

复杂度分析

  • 时间复杂度:O(n+q2^m),其中 n 为 nums 的长度,q 为 \max(\textit{nums}) 以内的 SF 个数,m=\pi(\max(\textit{nums})),\pi(N) 表示 N 以内的质数个数,对于本题的数据范围,q\le 18, m\le 10。
  • 空间复杂度:O(2^m)。
 Comments