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;
}
};