算法专练:状态压缩

1.LeetCode:1994. 好子集的数目

原题链接


        给你一个整数数组 nums 。如果 nums 的一个子集中,所有元素的乘积可以表示为一个或多个 互不相同的质数 的乘积,那么我们称它为 好子集 。

        比方说,如果 nums = [1, 2, 3, 4] :[2, 3] ,[1, 2, 3] 和 [1, 3] 是 好 子集,乘积分别为 6 = 23 ,6 = 23 和 3 = 3 。

        [1, 4] 和 [4] 不是 好 子集,因为乘积分别为 4 = 22 和 4 = 22 。

        请你返回 nums 中不同的 好 子集的数目对 1e9 + 7 取余 的结果。

        nums 中的 子集 是通过删除 nums 中一些(可能一个都不删除,也可能全部都删除)元素后剩余元素组成的数组。如果两个子集删除的下标不同,那么它们被视为不同的子集。

        示例 1:

        输入:nums = [1,2,3,4]

        输出:6

        示例 2:

        输入:nums = [4,2,3,15]

        输出:5

        提示:

        1 <= nums.length <= 10^5

        1 <= nums[i] <= 30


        我们要找出所有子数组的乘积能够被分解成多个质数的乘积的个数,一般这种题目我们容易想到的方法就是 dfs。再看题目的要求,每个质数只能选一次,下标不同的时候额外算作一个好数组。接着看数据量,数组元素最大为30,在[1,30]范围内的质数和不包含质因子的数我们可以提前找出来,这样就为我们的搜索提供了一些方便。

        在搜索之前我们需要做一些事情,由于数组的长度为1e5这样就说明如果是单纯的暴力搜索肯定是要T的,但是数组元素最大仅为30,而30以内的素数个数仅为10个,我们为每个质数安排一个合适且不重复的二进制掩码,这样他们的乘积就可以利用某个二进制来表示出来。而对于不含质因子的数,我们将其掩码设置为-1(二进制中为32个1)。其余的数组我们就将他的质因子储存在掩码上,(这里按位或可以把质因子的掩码都加在一起)代表他含有哪些质因子,过程如下:

int prime[] = {2,3,5,7,11,13,17,19,23,29};
int black[]={4,8,9,12,16,18,20,24,25,27,28};
void coding(){
        memset(code,0,sizeof(code));
        memset(hash,0,sizeof(hash));
        for(int i=0;i<10;++i){
            code[prime[i]]=1<<i;
        }
        for(int i=0;i<11;++i){
            code[black[i]]=-1;
        }
        for(int i=2;i<=30;++i){
            if(code[i]==0){
                for(int j=0;j<10;++j){
                    if(i%prime[j]==0){
                        code[i]|=code[prime[j]];
                    }
                }
            }
        }
        hash[31]++;
        code[31]=0;//特殊设置31是为了后面搜索的方便
    }


        在搜索之前我们要理解1这个数字的特殊性,由于选与不选1并不影响当前选择数组的乘积,所以对于数组中的1,当搜索到他的时候我们就有了两种选择,1就对答案产生了2^cnt[1]个影响为了不浪费时间我们对1特殊处理并在最后进行计算1的情况。而2 ^ [cnt1]可以利用二分快速幂来处理,具体原理如果有数学基础的话还是很容易看懂的,代码如下:

long long exp(long long a,long long b){
        if(b==0){
            return 1;
        }
        long long x=exp(a*a%mod,b>>1);
        if(b&1){
            x=x*a%mod;
        }   
        return x;
    }


        先对数组元素进行哈希表计数,接着开始从2搜索,并且维护目前选择数字的乘积状态mask,这里的mask并不是他们的乘积,而是存储了我们是否选择了这些数字,先看搜索代码:

long long dfs(int start,int mask){
        long long s=0;
        if(start==32){
            return 1;
        }
        for(int i=start;i<=31;++i){
            if(code[i]==-1){
                continue;
            }
            if(hash[i]==0){
                continue;
            }
            if(code[i]&mask){
                continue;
            }
            s+=dfs(i+1,mask|code[i])*hash[i]%mod;
            s%=mod;
        }
        return s;
    }


        我们这里的搜索方法是把数组的每个组合都尝试一次,于是要从2搜索到30,但是这样递归结束的条件就难以把控了,如果在start==31的时候结束递归显然是不可能的,因为30在我们选择了2,3,5的时候是进行不到下一层的(30=2 * 3 * 5)也就到不了递归结束的条件,所以要多进行一次搜索,不过这次我们的目的仅仅是让递归结束,搜索的过程中什么都不做,这也是hash[31]=1,code[31]=0的原因,(1是乘法的单位元,0是按位或的单位元)于是递归结束的条件就变为了start == 32。

        搜索的具体过程就是我们枚举除了1之外所有nums的元素的组合,当数组不含有i,或者i没有质因子,或者之前选择过i的质因子的话不选择i,尝试组合下一个数。如果这些条件都符合,我们对答案进行累加,也就是s+=dfs(start+1,mask|coed[i])*hash[i]%mod,如果选择了当前的数字就尝试下个数字,并在mask上按位或上当前数字的掩码代表选择了他,选择了他自然就要乘上他的方案数并模上mod。当所有的结果都算出了之后我们减去一个都不选择的情况再乘上2^cnt[1],就是最终的答案。

int prime[] = {2,3,5,7,11,13,17,19,23,29};
int black[]={4,8,9,12,16,18,20,24,25,27,28};
class Solution {
    #define mod 1000000007
    int code[32];
    int hash[32];

    void coding(){
        memset(code,0,sizeof(code));
        memset(hash,0,sizeof(hash));
        for(int i=0;i<10;++i){
            code[prime[i]]=1<<i;
        }
        for(int i=0;i<11;++i){
            code[black[i]]=-1;
        }
        for(int i=2;i<=30;++i){
            if(code[i]==0){
                for(int j=0;j<10;++j){
                    if(i%prime[j]==0){
                        code[i]|=code[prime[j]];
                    }
                }
            }
        }
        hash[31]++;
        code[31]=0;
    }
    long long dfs(int start,int mask){
        long long s=0;
        if(start==32){
            return 1;
        }
        for(int i=start;i<=31;++i){
            if(code[i]==-1){
                continue;
            }
            if(hash[i]==0){
                continue;
            }
            if(code[i]&mask){
                continue;
            }
            s+=dfs(i+1,mask|code[i])*hash[i]%mod;
            s%=mod;
        }
        return s;
    }
    long long exp(long long a,long long b){
        if(b==0){
            return 1;
        }
        long long x=exp(a*a%mod,b>>1);
        if(b&1){
            x=x*a%mod;
        }   
        return x;
    }
public:
    int numberOfGoodSubsets(vector<int>& nums) {
        coding();
        for(int i=0;i<nums.size();++i){
            ++hash[nums[i]];
        }
        return exp(2,hash[1]) * (dfs(2,0)-1) % mod;
    }
};
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值