加载中...

子集型状压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的补集),即把mask2mask1中扣去,剩下的集合

差集 = 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

其中masknum元素对应的二进制数,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
posted @ 2023-02-20 19:25  Chenjq12  阅读(115)  评论(0)    收藏  举报