子集型状压DP
问题
给你一个正整数数组 nums 。
如果数组 nums 的子集中的元素乘积是一个 无平方因子数 ,则认为该子集是一个 无平方 子集。
无平方因子数 是无法被除 1 之外任何平方数整除的数字。
返回数组 nums 中 无平方 且 非空 的子集数目。因为答案可能很大,返回对 109 + 7 取余的结果。
nums 的 非空子集 是可以由删除 nums 中一些元素(可以不删除,但不能全部删除)得到的一个数组。如果构成两个子集时选择删除的下标不同,则认为这两个子集不同。
示例
输入:nums = [3,4,4,5]
输出:3
解释:示例中有 3 个无平方子集:
- 由第 0 个元素 [3] 组成的子集。其元素的乘积是 3 ,这是一个无平方因子数。
- 由第 3 个元素 [5] 组成的子集。其元素的乘积是 5 ,这是一个无平方因子数。
- 由第 0 个和第 3 个元素 [3,5] 组成的子集。其元素的乘积是 15 ,这是一个无平方因子数。
可以证明给定数组中不存在超过 3 个无平方子集。
策略
这道题考察的是:子集型状态压缩DP
- 什么是子集
子集型状态压缩DP,一般是利用二进制数来代表一个集合,第i位为1代表包含第i个元素,0代表不包含这个元素。
例如:【1, 2, 3, 4, 5】,0b00011 = 3表示集合【1,2】,0b11100 = 28表示集合【3, 4, 5】
sub 是state 的一个子集,当且仅当 sub 中任意的 1在state 中的对应位置均为 1
- 一些常用的操作:
判断
mask2表示的集合 是不是 mask1 表示的集合 的子集
if ((mask1 | mask2) == mask1) // True : 是 False : 不是
如果
mask2表示的集合 是mask1表示的集合 的子集,求二者的差集(mask2的补集),即把mask2从mask1中扣去,剩下的集合
差集 = mask1 ^ mask2
在状压DP中常用操作:dp[state] = min(dp[state], dp[state ^ sub]) 表示包含sub 和不包含sub两种情况
枚举一个集合的子集
subset = mask
while True:
// subset 是 mask 的一个子集,可以用其进行状态转移
...
// 使用按位与运算在 O(1) 的时间快速得到下一个(即更小的)mask 的子集
subset = (subset - 1) & mask
if subset == mask: break
两个集合求交集,并集
// 交集
mask1 & mask2
// 并集
mask1 | mask2
问题一:如何将乘积和集合做映射?
数据的取值范围是[1, 30],做一个长度位31的映射表NSQ_TO_MASK,把 2 - 30的每一个数分解质因数,如果有两个以上相同的质因数,那它一定不是无平方因子数,记为-1,否则就把所有质因数集合,表示成一个二进制数,注意这里的mask代表的是PRIMES数组里边的数哪个选,哪个不选,而不是原数组的。
例如:
| PRIMES | 2 | 3 | 5 | 7 | …… | result |
|---|---|---|---|---|---|---|
| 3 = 3 | 0 | 1 | 0 | 0 | 0b10 | |
| 4 = 2 * 2 | 2 | 0 | 0 | 0 | -1 | |
| 6 = 2 * 3 | 1 | 1 | 0 | 0 | 0b11 | |
| 15 = 5 * 3 | 0 | 1 | 1 | 0 | 0b110 |
实现:
# 数据范围 1 - 30
PRIMES = [2, 3, 5, 7, 9, 11, 13, 17, 19, 23, 29]
NSQ_TO_MASK = [0] * 31
for i in range(2, 31):
for j, p in enumerate(PRIMES):
if i % p == 0: # p 是 i 的质因数
if i % (p * p): # p^2 是 i 的因数
NSQ_TO_MASK[i] = -1 # 不是无平方因子数标记为-1
break
NSQ_TO_MASK |= 1 << j # 第j位置一,表示把第j个质数加入集合
问题二:怎么找符合条件的子集?
枚举数组nums的元素,如果他是无平方因子数,那么就找一个集合和他没有交集,二者的并集的乘积一定也是无平方因子数,即f[mask | j] = (f [mask | j] + f[j] * c ) % MOD
其中mask是num元素对应的二进制数,c表示这个元素的个数,j表示和mask没有交集的集合
问题三:如何找到所有和mask没有交集的集合?
mask的补集的子集都和mask没有交集,并且是全部和mask没有交集的集合,因此直接枚举mask的补集的子集
问题四:这么做不会超出nums集合吗?
是会的,但是由于我们给f[j]的初值是零,如果j表示的集合不是nums的子集,那么mask | j也不是nums的子集,f[j]会一直等于0,f[mask | j]也一直等于0,对结果没有影响。
Code
class Solution:
def squareFreeSubsets(self, nums: List[int]) -> int:
PRIMES = [2, 3, 5, 7, 9, 11, 13, 17, 19, 23, 29]
NSQ_TO_MASK = [0] * 31
for i in range(2, 31):
for j, p in enumerate(PRIMES):
if i % p == 0: # p 是 i 的质因数
if i % (p * p) == 0: # p^2 是 i 的因数
NSQ_TO_MASK[i] = -1 # 不是无平方因子数标记为-1
break
NSQ_TO_MASK[i] |= 1 << j # 第j位置一,表示把第j个质数加入集合
MOD = 10 ** 9 + 7
cnt = Counter(nums)
M = 1 << len(PRIMES)
f = [0] * M
f[0] = 1
for x, c in cnt.items():
mask = NSQ_TO_MASK[x]
if mask > 0 : #是NSQ
sub = other = (M - 1) ^ mask
while True:
f[mask | sub] = (f[mask | sub] + f[sub] * c) % MOD
sub = (sub - 1) & other
if sub == other: break
return (sum(f) * pow(2, cnt[1], MOD) - 1) % MOD

浙公网安备 33010602011771号