[leetcode]1994好子集的数目,状态dp

题意

给出一个数组nums,找出数组中的“好子集”的个数,好子集的定义为:

  1. 子集的元素都在nums中
  2. 好子集元素的乘积可以表示为一个质数或多个互不相同的质数的乘积
  3. 子集元素在nums中的下标不同,可以被视为不同的子集
    如[1,2,2,3]的子集有:[], [1],[2],[2],[3],[1,2],[1,2],[1,3],[2,2],[2,3],[2,3],[1,2,2],[1,2,3],[1,2,3],[2,2,3],[1,2,2,3],好子集有:[2],[2],[3],[1,2],[1,2],[1,3],[2,3],[2,3],[1,2,3],[1,2,3],
    这里出现的两个[1,2],[2,3],[1,2,3],就是因为数组有两个下标不同的2,被视为两个不同的子集。
    答案对1e9+7取模

思路

先考虑数据范围,nums的个数最多的100000,但nums的值最大也就30。
在1到30中分开情况:

  1. 拥有相同质数的乘积的值有:4,8,9,12,16,18,20,24,25,27,28
  2. 质数:2,3,5,7,11,13,17,19,23,29
  3. 多个互不相同的质数:6,10,14,15,21,22,26,30
  4. 1不是质数也不是合数

假设数组中只有2和3,但有x个2和y个3,答案是什么?
2是质数,3也是质数,[2,3]也是符合题目定义的好子集,那么[2]是好子集可以出现x次,[3]也是好子集可以出现y次,[2,3]可以出现 xy 次。
答案是(x+y+x
y)%mod

这里可以看出,要保证互不相同的质数,用过的质数就不能同一子集中再次使用,但相同的值可以作为下标不同的情况多次出现在好子集中。

因为1到30中的质数只用10个,能出现的不同质数乘积的所有值能控制在2的十次方内,可以使用位运算实现状态dp,dp[i]表示质数使用状态为i的时候,好子集的方案数,i的定义为:

  1. 存在某个数字tar,满足使用了i状态的质数,且每个质数仅使用一次。
  2. 若i & ( 1 << j) != 0,则说明tar能被第j个质数整除,tar使用了第j个质数
  3. 若i & ( 1 << j) == 0,则说明tar不能被第j个质数整除,tar未使用第j个质数

i == 6,化为二进制就表示为110,使用了第1,2个质数(从0数起),就dp[6]表示乘积为3*5 == 15的好子集的方案数。
然后这里讨论dp[0]的意义,这里表示没有使用任何质数,那乘积就是1,而1的个数是会影响到i!=0的所有情况的,如果nums中没有出现1,那么dp[0]就等于1,但nums出现1,那就要考虑用多少个1和用哪些1,因为x=1*x=1*1*x=1*1*1*x,1不改变乘积的结果,它可以被随意使用,所以dp[0]就应该表示为2的n次方,n为1出现的次数。(这里用了快速幂取模,但官方教程没用快速幂)
统计1到30的出现次数,初始化dp[0],i遍历每个值:
没出现的值以及出现多个相同质数的值过滤掉(代码26和32行),用k表示数字i的质数使用状态,dp出所有包含k的状态j,状态转移方程为:dp[j] +=dp[j^k]*a[i]表示从没有使用到k状态的好子集加上数字i后,j状态所增加的方案数。遍历i要从小到大,遍历j的时候要大到小,避免后效性。

代码

class Solution {
public:
    long long primes[10]={2,3,5,7,11,13,17,19,23,29};
    long long mod = 1e9+7;
    long long myPow(long long a,long long b){
        long long res = 1;
        while(b){
            if(b&1){
                res *= a;
                res %= mod;
            }
            a *= a;
            a %= mod;
            b >>= 1;
        }
        return res;
    }
    int numberOfGoodSubsets(vector<int>& nums) {
        vector<long long> a(31,0);
        for(int x:nums){
            ++a[x];
        }
        vector<long long> dp(1<<10,0);
        dp[0]=myPow(2ll,a[1]);
        for(int i = 2;i <= 30; ++i){
            if(!a[i])continue;
            int flag = 0;
            int k = 0;
            for(int j = 0;j < 10; ++j){
                if(i % primes[j] == 0){
                    if(i % (primes[j]*primes[j])==0){
                        flag = 1;
                        break;
                    }
                    k |= (1<<j);
                }
            }
            if(flag)continue;
            for(int j = (1<<10)-1;j > 0; --j){
                if((j&k)==k){
                    dp[j] +=dp[j^k]*a[i];
                    dp[j] %= mod;
                }
            }
        }
        long long ans = 0;
        for(int i = 1;i < (1<<10); ++i){
            ans += dp[i];
            ans %= mod;
        }
        return (int)ans;
    }
};
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值