LeetCode1994. 好子集的数目—状压DP

在这里插入图片描述
好子集的数目

  本题的数据范围比较小决定了可以使用状态压缩dp。
  本题要把子集中的所有数的乘积拆成不同的质因子,必须子集中都持有不同的质因子,并且每个元素子集不能持有重复的质因子。
  由于数据范围是1~30,所以总共只有10个质数,我们可以用一个长度为10的二进制串表示我们子集中每个数持有的质因子的情况。
  然后考虑定义状态为 f ( i , m a s k ) f(i, mask) f(i,mask)表示考虑2~i的数,子集的质因子分布情况是 m a s k mask mask的总子集方案数。
  若当前遍历的数i不存在于数组中,则没法转移这个数;
  若当前遍历的数i持有重复的质因子,则没法选当前数入子集,状态转移方程为 f [ i ] [ m a s k ] = f [ i − 1 ] [ m a s k ] f[i][mask] = f[i - 1][mask] f[i][mask]=f[i1][mask];
  若当前遍历的数不持有重复的质因子,设当前数质因子的持有情况是 s u b s e t subset subset,则可以选当前数入子集,有 c n t [ i ] cnt[i] cnt[i]个当前数i,则有 c n t [ i ] cnt[i] cnt[i]种选择,也可以不选,状态转移方程为:
f [ i ] [ m a s k ] = f [ i − 1 ] [ m a s k ] + c n t [ i ] ∗ f [ i − 1 ] [ m a s k s u b s e t ] , i f ( ( m a s k & s u b s e t ) = = s u b s e t ) f[i][mask] = f[i - 1][mask] + cnt[i] * f[i - 1][mask subset],if ((mask \& subset) == subset) f[i][mask]=f[i1][mask]+cnt[i]f[i1][masksubset],if((mask&subset)==subset)
  这里利用了异或可以把相同的位消掉,不同的位保留为1的性质,并且只有当前考虑的质因子分布中包含了i的质因子分布才行,对应到二进制位上, s u b s e t subset subset必须是 m a s k mask mask的子集。

class Solution {
public:
    int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
    static const int N = 1e9 + 7;
    int numberOfGoodSubsets(vector<int>& nums) 
    {
        // 先看数据范围 1 <= nums[i] <= 30 在这 1~30中 只有这些质数:
        // 2 3 5 7 11 13 17 19 23 29 总共10个
        // 本题要求把乘积拆成一个或多个互不相同的质因子的乘积
        // 我们可以用一个10位的二进制数字mask来表示每个质因子是否已经被选过了
        // 定义f[i][mask]表示在质因子选择情况是mask的情况下 考虑2~i范围内的数一共有多少种方案
        // 答案就是求和f[30][k] k 从1到1 << 10 表示所有质数选择情况
        // 状态转移方程:若i含有多个相同的质因子 则不能选i 方程为f[i][mask] = f[i - 1][mask]
        // 若i含有多个不同的质因子 记它们的质因子分布情况是subset(一个10位的二进制数字)
        // 若subset是mask的子集,即mask & subset == subset, 这样才能转移
        // 状态转移方程就为 f[i][mask] = f[i - 1][mask] + f[i - 1][mask ^ subset] * cnt[i]
        // 可以选cnt[i]个i,所以还要乘上cnt[i]
        // 异或使相同的全变0,不相同的全变1
        // 初始条件f[1][0] = 2^(cnt[1]),因为每个1都可以选或不选,增加2^(cnt[1])个情况
        vector<int> cnt(31);
        // 计数
        for (auto l : nums)
        {
            ++cnt[l];
        }
        int mask_max = 1 << 10;
        // 状态转移的数组 考虑到仅和前继有关 所以用一个一维数组优化
        vector<int> dp(mask_max);
        // 初始条件
        dp[0] = 1;// 先初始化成1 这里的dp[0]就是f[1][0]
        // 不管有没有1 我们都可以假设有1个 这样不影响乘法计数
        // 然后把dp[0]维护为2^(cnt[1])
        for (int i = 0; i < cnt[1]; ++i)
        {
            dp[0] = dp[0] * 2 % N;
        }
        for (int i = 2; i <= 30; ++i)
        {
            // 若数组中没这个元素 也没必要选了
            if (!cnt[i]) continue;
            // 看看它是否持有重复的质因子 同时统计它质因子出现的情况
            int x = i, subset = 0;
            bool flag = true;
            for (int j = 0; j < 10; ++j)
            {
                if (x % (prime[j] * prime[j]) == 0)
                {
                    flag = false;
                    break;
                }
                if (x % prime[j] == 0)
                {
                    subset |= (1 << j);
                }
            }
            // 如果持有重复质因子 就不考虑这个数了
            if (flag == false)
            {
                continue;
            }
            // 否则进行动态规划
            // 由于subset是mask的子集 所以subset ^ mask一定比 mask小
            // 所以一维数组的动态规划优化为了保证计算到dp[mask ^ subset]
            // 还是上一层的f[i - 1][mask ^ subset] 所以循环从mask_max - 1往小走
            for (int mask = mask_max - 1; mask > 0; --mask)
            {
                if ((subset & mask) == subset)
                {
                    dp[mask] = 
                    (dp[mask] + static_cast<long long>(dp[mask ^ subset]) * cnt[i]) % N;
                }
            }
        }
        int ret = 0;
        for (int mask = 1; mask < mask_max; ++mask)
        {
            ret = (ret + dp[mask]) % N;
        }
        return ret;
    }
};

  本题还可以剪枝,本题中 子集中不可能插入 4 、 8 、 9 、 12 、 16 、 18 、 20 、 24 、 25 、 27 、 28 4、8、9、12、 16、 18、 20、 24、 25、 27、 28 4891216182024252728这些元素,因为他们都含有质数的平方 2 ∗ 2 2*2 22或者 3 ∗ 3 3*3 33 或者 5 ∗ 5 5*5 55,利用一个哈希表提前存好这些数字,若hash.count(i) != 0直接continue即可。

class Solution {
public:
    int prime[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
    int mask_max = (1 << 10) - 1;
    unordered_set<int> hash = {4, 8, 9, 12, 16, 18, 20, 24, 25, 27, 28};
    static const int N = 1e9 + 7;
    int numberOfGoodSubsets(vector<int>& nums) 
    {
        vector<int> cnt(31);
        for (int num : nums){
            ++cnt[num];
        }
        vector<int> dp(mask_max + 1);
        dp[0] = 1;
        for (int i = 0; i < cnt[1]; ++i)
        {
            dp[0] = dp[0] * 2 % N;
        }
        for (int i = 2; i <= 30; ++i)
        {
            if (cnt[i] == 0 || hash.count(i) != 0) continue;
            int x = i, subset = 0;
            bool flag = true;
            for (int j = 0; j < 10; ++j)
            {
                if (x % (prime[j] * prime[j]) == 0){
                    flag = false;
                    break;
                }
                if ((x % prime[j]) == 0)
                {
                    subset |= (1 << j);
                }
            }
            if (flag == false) continue;
            for (int mask = mask_max; mask > 0; --mask)
            {
                if ((mask & subset) == subset)
                {
                    dp[mask] = 
                    (dp[mask] + static_cast<long long>(dp[mask ^ subset]) * cnt[i]) % N;
                }
            }
        }
        int ret = 0;
        for (int mask = 1; mask <= mask_max; ++mask)
        {
            ret = (ret + dp[mask]) % N;
        }
        return ret;
    }
};
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值