1994. The Number of Good Subsets
Problem Description
You have an array of integers called nums
. The task is to find the number of different subsets of nums
that are considered "good." A "good" subset is one where the product of its elements can be written as the product of one or more distinct prime numbers. Specifically:
- If a subset's product includes a repeated prime factor (like
2 * 2
), it's not a "good" subset. 1
has no prime factors and can be included in a "good" subset; it doesn't affect the product's prime factorization.- Different subsets are defined as those made by deleting different elements from
nums
.
The answer must be provided modulo (10^9 + 7
), which is a common way to keep the numbers from getting too large by using the remainder after division by a large prime number.
For example, in an array [1, 2, 3, 4]
, the subset [2, 3]
is good because 2 * 3 = 6
(product of distinct primes), but [4]
is not good, since 4 = 2 * 2
(repeated prime).
Intuition
The intuition behind the solution is rooted in combinatorics and bitwise operations. Firstly, notice that the maximum value in nums
is specified as 30, which simplifies our problem since there are only a few primes within this range.
Here's how we can think about the problem:
- Count the frequencies of each number in
nums
using a Counter. - Precalculate a list of primes that we will use to check against the elements in
nums
. - Use a bitmask to represent the inclusion of a certain prime factor in a subset product. For example, if the bitmask is
0101
, it could represent that the product has prime factors of the 1st and 3rd prime numbers in ourprimes
list. - Initialize an array
f
wheref[i]
(wherei
is a bitmask) will represent the number of ways to get a product that has the prime factors represented by the bitmaski
. - Handle the special case for
1
separately, since it doesn't affect the product but it can be included or not, essentially doubling the number of subsets for each possible product. - Iterate through all numbers from
2
to30
and for each, calculate their bitmask of prime factors if they have no repeated prime factors (like4
,9
,25
, etc., which are skipped). - Use dynamic programming to update the array
f
when considering the inclusion of each number with no repeated prime factors as you iterate through all possible states (bitmasks). - The final answer is the sum of all values of
f
exceptf[0]
, which is the empty subset, modulo10^9 + 7
.
The solution leverages bite masks to encode the presence of prime factors elegantly and uses dynamic programming to build up the counts of valid subsets, taking into account the combination of present factors without duplication.
Learn more about Math, Dynamic Programming and Bitmask patterns.
Solution Approach
The solution approach involves the following steps:
-
Count frequencies: Create a count of all the numbers
nums
using theCounter
class from Python'scollections
. This step is necessary to handle duplicated numbers innums
.cnt = Counter(nums)
-
Modulo arithmetic: Define the modulo
mod = 10**9 + 7
because we want the results modulo10^9 + 7
.mod = 10**9 + 7
-
Bitmasking for prime factors:
- Define
primes
as an array of prime numbers that are less than 30 since the question limits the numbers innums
up to 30. - Create an array
f
with size1 << len(primes)
(which is2^n
wheren
is the number of primes).f
uses bitmasks to represent subsets of primes included in the product. For example,f[0b1010]
would represent the number of subsets whose product is made up of the 2nd and 4th prime numbers in theprimes
list.
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] f = [0] * (1 << len(primes))
- Define
-
Handling the special case for number
1
:- The product of any "good" subset doesn't change if we include or exclude
1
. Thus, any subset that includes1
can appear in2^count[1]
different ways. We initializef[0]
(which includes no prime factors) to this value.
f[0] = pow(2, cnt[1], mod)
- The product of any "good" subset doesn't change if we include or exclude
-
- Iterate through numbers
x
from2
to30
. If a number has a square of a prime as a factor (like4
,9
,25
), we skip it since it is not a "good" subset. - Calculate the bitmask for the prime factors of
x
. Ifx
is divisible by a prime at indexi
, we set thei
th bit ofmask
to1
. - We then iterate through all possible states (from
(1 << n) - 1
to1
) to update ourf
using dynamic programming. If a state contains all bits in themask
(checked withstate & mask == mask
), we can includex
in this state. We then updatef[state]
by adding the ways to form the current product from the state withoutx
times the count ofx
. - Finally, the
f[state ^ mask]
refers to the current state minus the prime factors ofx
.
for x in range(2, 31): if cnt[x] != 0 and x % 4 != 0 and x % 9 != 0 and x % 25 != 0: mask = 0 for i, p in enumerate(primes): if x % p == 0: mask |= 1 << i for state in range((1 << n) - 1, 0, -1): if state & mask == mask: f[state] = (f[state] + cnt[x] * f[state ^ mask]) % mod
- Iterate through numbers
-
Deriving the answer:
- Iterate over all
f
but skip the0
state because it includes no primes. Sum thef[i]
values to get the total number of "good" subsets, and take the result modulomod
.
answer = sum(f[i] for i in range(1, 1 << n)) % mod
- Iterate over all
-
The
answer
is returned as the final number of "good" subsets modulo10^9 + 7
.
By combining these steps—specifically the use of bitmasking, dynamic programming, and special handling for 1
—the algorithm efficiently calculates the number of "good" subsets within the given constraints.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's illustrate the solution approach using a small example: consider the array nums = [1, 2, 3, 4, 6]
.
-
Count frequencies: Using a
Counter
, we find that the frequencies are:{1: 1, 2: 1, 3: 1, 4: 1, 6: 1}
-
Modulo arithmetic: The results are to be presented modulo
mod = 10**9 + 7
. -
Bitmasking for prime factors:
- Our
primes
array with primes less than 30 is[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
. - We create an array
f
of size1 << len(primes)
(1024 in this case).
- Our
-
Handling the special case for number
1
:- Since 1 doesn't affect the product's prime factorization, there are
2^count[1] = 2
ways to include it in any subset. We setf[0] = 2
.
- Since 1 doesn't affect the product's prime factorization, there are
-
Dynamic Programming:
- First we consider
2
: it's a prime, so its bitmask is0b0000000001
. There are1
way to have a subset with a product of2
(ignoring1
for now), sof[0b0000000001] = 1
. - Next,
3
is prime and its bitmask is0b0000000010
. There is1
way for a subset with a product of3
, sof[0b0000000010] = 1
. 4
is not considered because it's not a product of distinct primes, being2 * 2
.- For
6
(which is2 * 3
), its mask is0b0000000011
. However, there is no state to update yet that includes both2
and3
, sof[0b0000000011]
remains0
for now. - At the end of the first iteration,
f
looks like this (showing non-zero values only):f[0b0000000000] = 2 (from the special case of '1') f[0b0000000001] = 1 (from '2') f[0b0000000010] = 1 (from '3')
- First we consider
-
Deriving the answer:
- Summing the non-zero
f[i]
values, we get2 + 1 + 1 = 4
, which we need to present modulo10^9 + 7
.
- Summing the non-zero
-
Final Answer: After iterating through the other numbers in
nums
and updating thef
array accordingly, the final answer would be the sum of all validf[i]
values after completing the dynamic programming (modulomod
). In our small example case, we would need to continue the dynamic programming steps for6
, which would increase the count off[0b0000000011]
, and then take the sum of thef
array (excludingf[0]
) modulomod
.
In a fully worked-through solution with the remaining numbers, we would use the above process to dynamically update the f
array with all combinations of primes represented by the numbers in nums
. This method ensures, through the use of bitmasks and the dynamic array f
, that only "good" subsets (those with unique prime factors) are considered in the final count, adhering to the rules of the problem.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def numberOfGoodSubsets(self, nums: List[int]) -> int:
5 primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] # List of primes up to 29
6 count = Counter(nums) # Count frequency of each number in 'nums'
7 mod = 10**9 + 7 # Define the modulus for taking the result modulo 1e9+7
8 num_primes = len(primes)
9 dp = [0] * (1 << num_primes) # dp[state] will keep track of the count of good subsets for a given state
10 dp[0] = pow(2, count[1], mod) # Initiate the dp[0] state with the count of subsets containing '1's
11
12 # Iterate through numbers 2 to 30 to update dp with good subsets counts
13 for x in range(2, 31):
14 if count[x] == 0 or x % 4 == 0 or x % 9 == 0 or x % 25 == 0:
15 continue # Skip if the number is not present or is a square
16
17 bit_mask = 0 # Will hold unique prime factors of 'x' as bit positions
18 for i, prime in enumerate(primes):
19 if x % prime == 0:
20 bit_mask |= 1 << i # Set the bit corresponding to the prime number
21
22 # Traverse all combinations of prime factors (states) and update counts
23 for state in range((1 << num_primes) - 1, 0, -1):
24 if state & bit_mask == bit_mask: # Check if 'state' includes all prime factors of 'x'
25 dp[state] = (dp[state] + count[x] * dp[state ^ bit_mask]) % mod
26
27 # Sum up all counts except the empty subset (which is dp[0])
28 result = sum(dp[i] for i in range(1, 1 << num_primes)) % mod
29 return result
30
1class Solution {
2 public int numberOfGoodSubsets(int[] nums) {
3 // Initialize prime numbers up to 30 (inclusive)
4 int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
5
6 // Count the frequency of each number in the given array
7 int[] count = new int[31];
8 for (int num : nums) {
9 ++count[num];
10 }
11
12 // Define the modulo for performing operations under, to avoid overflow issues
13 final int MOD = (int) 1e9 + 7;
14
15 // Initialize the number of primes
16 int numPrimes = primes.length;
17
18 // Create a dynamic programming array to store ways of forming a good subset ending with the prime bitmask
19 long[] dp = new long[1 << numPrimes];
20 dp[0] = 1;
21
22 // Account for the number 1 which can be included in any subset, contributing to the count by multiplying by 2
23 for (int i = 0; i < count[1]; ++i) {
24 dp[0] = (dp[0] * 2) % MOD;
25 }
26
27 // Go through the numbers 2 to 30 and update the dp array for each possible subset
28 for (int x = 2; x < 31; ++x) {
29 // Skip numbers with zero count or that are squares of other primes
30 if (count[x] == 0 || x % 4 == 0 || x % 9 == 0 || x % 25 == 0) {
31 continue;
32 }
33
34 // Calculate the bitmask for the current number based on its prime factors
35 int mask = 0;
36 for (int i = 0; i < numPrimes; ++i) {
37 if (x % primes[i] == 0) {
38 mask |= 1 << i;
39 }
40 }
41
42 // Update the dp array from the end to the beginning to avoid revisiting updated states
43 for (int state = (1 << numPrimes) - 1; state > 0; --state) {
44 // Only update the dp array if the current state contains the prime factors of x
45 if ((state & mask) == mask) {
46 dp[state] = (dp[state] + count[x] * dp[state ^ mask]) % MOD;
47 }
48 }
49 }
50
51 // Sum up all the dp values except for the empty subset to get the final answer
52 long answer = 0;
53 for (int i = 1; i < 1 << numPrimes; ++i) {
54 answer = (answer + dp[i]) % MOD;
55 }
56
57 // Return the final answer as an integer
58 return (int) answer;
59 }
60}
61
1class Solution {
2public:
3 int numberOfGoodSubsets(vector<int>& nums) {
4 // Array of the first 10 prime numbers
5 int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
6
7 // Count array initialized to zero for numbers 1 to 30
8 int count[31] = {};
9 // Count the frequency of each number in the nums array
10 for (int x : nums) {
11 ++count[x];
12 }
13
14 // Number of prime numbers considered
15 int numPrimes = 10;
16
17 // Modulus for handling large numbers
18 const int mod = 1e9 + 7;
19
20 // Dynamic programming (DP) table to store the number of good subsets per state
21 vector<long long> dp(1 << numPrimes);
22 dp[0] = 1;
23
24 // Account for the number of times 1 appears, since it can be part of any subset
25 for (int i = 0; i < count[1]; ++i) {
26 dp[0] = dp[0] * 2 % mod;
27 }
28
29 // Iterate through 2 to 30 to update the DP table accordingly
30 for (int x = 2; x < 31; ++x) {
31 // Skip processing if the count is 0 or if the number has duplicate prime factors
32 if (count[x] == 0 || x % 4 == 0 || x % 9 == 0 || x % 25 == 0) {
33 continue;
34 }
35
36 // Binary representation of the number's prime factors
37 int mask = 0;
38 for (int i = 0; i < numPrimes; ++i) {
39 if (x % primes[i] == 0) {
40 mask |= 1 << i;
41 }
42 }
43
44 // Update the DP table states for the current number with its count
45 for (int state = (1 << numPrimes) - 1; state > 0; --state) {
46 if ((state & mask) == mask) {
47 dp[state] = (dp[state] + static_cast<long long>(count[x]) * dp[state ^ mask]) % mod;
48 }
49 }
50 }
51
52 // Calculate the final answer by summing up all good subsets excluding the empty set
53 long long answer = 0;
54 for (int i = 1; i < (1 << numPrimes); ++i) {
55 answer = (answer + dp[i]) % mod;
56 }
57
58 // Return the computed number of good subsets as an integer
59 return static_cast<int>(answer);
60 }
61};
62
1// Constants for prime numbers and modulus
2const primes: number[] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
3const mod: number = 1e9 + 7;
4
5// Define the count array and initialize it to zeroes
6let count: number[] = new Array(31).fill(0);
7
8// Function to count the frequency of each number in the nums array
9function countFrequencies(nums: number[]): void {
10 for (let x of nums) {
11 ++count[x];
12 }
13}
14
15// Function to compute the number of good subsets
16function numberOfGoodSubsets(nums: number[]): number {
17 countFrequencies(nums);
18
19 const numPrimes: number = primes.length;
20
21 // DP table to store the number of good subsets for each state
22 let dp: bigint[] = new Array(1 << numPrimes).fill(BigInt(0));
23 dp[0] = BigInt(1);
24
25 // Account for the presence of 1, as it can be part of any subset
26 for (let i = 0; i < count[1]; ++i) {
27 dp[0] = dp[0] * BigInt(2) % BigInt(mod);
28 }
29
30 // Iterate through numbers 2 to 30, updating the DP table for each
31 for (let x = 2; x <= 30; ++x) {
32 // Skip if count is zero or if the number has repeated prime factors
33 if (count[x] === 0 || x % 4 === 0 || x % 9 === 0 || x % 25 === 0) continue;
34
35 // Create bitmask representation of the number's prime factors
36 let mask: number = 0;
37 for (let i = 0; i < numPrimes; ++i) {
38 if (x % primes[i] === 0) {
39 mask |= 1 << i;
40 }
41 }
42
43 // Update DP table states for the current number, considering its count
44 for (let state = (1 << numPrimes) - 1; state > 0; --state) {
45 if ((state & mask) === mask) {
46 dp[state] = (dp[state] + BigInt(count[x]) * dp[state ^ mask]) % BigInt(mod);
47 }
48 }
49 }
50
51 // Sum up all good subsets, excluding the empty set
52 let answer: bigint = BigInt(0);
53 for (let i = 1; i < (1 << numPrimes); ++i) {
54 answer = (answer + dp[i]) % BigInt(mod);
55 }
56
57 // Return the computed number of good subsets
58 return Number(answer);
59}
60
61// Example usage
62// const nums: number[] = [1, 2, 3, ...]; // Fill with your set of numbers
63// const result: number = numberOfGoodSubsets(nums);
64
Time and Space Complexity
Time Complexity
The time complexity of the given code is determined by several nested loops and operations on the Counter object.
- Counting the frequency of each number takes
O(n)
, wheren
is the length of thenums
list. - Calculating
pow(2, cnt[1], mod)
isO(log(cnt[1]))
, which is the complexity of the exponentiation operation. - The loop from 2 to 30 for each number
x
is run 29 times (O(1)
since it's a constant), and within this loop:- Checking whether
x
is divisible by 4, 9, or 25 isO(1)
. - Constructing the
mask
requires iterating over the list of primes, which isO(m)
wherem
is the number of primes (len(primes)
).
- Checking whether
- The most significant loop is the nested loop that iterates over all states of the
f
array. There are(1 << n)
states, which is2^n
, iterated for eachx
. The bitwise operations inside the loop areO(1)
operations. Therefore, the nested loop's complexity isO(2^n * m)
.
Overall, the most time-consuming part is the nested loop with a complexity of O(2^n * m)
, where n
is the number of primes considered (n = 10
here) and m
is the count of numbers between 2 and 30 for which frequency is calculated. Consequently, the overall time complexity is O(2^n * m)
which simplifies to O(2^10 * 29)
.
Space Complexity
The space complexity of the code is determined by the additional data structures that are used:
- The
cnt
Counter object has at most 30 key-value pairs corresponding to the numbers from 1 to 30, thusO(1)
space. - The
f
array has(1 << n)
elements, which is2^n
, yieldingO(2^n)
space complexity.
The dominant factor is the space required for the f
array. Hence, the overall space complexity is O(2^n)
, where n
is the number of primes.
Learn more about how to find time and space complexity quickly using problem constraints.
A person thinks of a number between 1 and 1000. You may ask any number questions to them, provided that the question can be answered with either "yes" or "no".
What is the minimum number of questions you needed to ask so that you are guaranteed to know the number that the person is thinking?
Recommended Readings
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Bitmask and Dynamic Programming Bit manipulation is a crucial aspect of computer programming and one of the most powerful tools for bit manipulation is bitmasks Let's first understand what a bit is A bit is a binary digit It's the smallest piece of data in a computer and can be
Want a Structured Path to Master System Design Too? Don’t Miss This!