1994. The Number of Good Subsets
Problem Description
You are given an integer array nums
. A subset of nums
is called good if the product of all its elements can be expressed as a product of distinct prime numbers (each prime appears at most once in the factorization).
In other words, a subset is good if its product has no repeated prime factors - each prime in the prime factorization appears with a power of exactly 1.
For example, with nums = [1, 2, 3, 4]
:
[2, 3]
is good because its product is6 = 2 × 3
(two distinct primes)[1, 2, 3]
is good because its product is6 = 2 × 3
(the 1 doesn't affect the product)[1, 3]
is good because its product is3
(one prime)[1, 4]
is NOT good because its product is4 = 2²
(prime 2 appears twice)[4]
is NOT good because4 = 2²
(prime 2 appears twice)
A number whose prime factorization contains only distinct primes (each with power 1) is called a square-free number. So essentially, we need to count subsets whose product is square-free.
The task is to count the total number of different good subsets in nums
. Return the answer modulo 10⁹ + 7
.
Note that:
- A subset can be obtained by selecting any combination of elements from
nums
(including the empty subset, though it won't be good) - Two subsets are considered different if they are formed by selecting different indices from the array, even if they contain the same values
- The number 1 is special - it doesn't affect the product, so it can be freely included or excluded from any good subset
Intuition
The key insight is that we need subsets whose product is square-free (no repeated prime factors). Since nums
contains integers from 1 to 30, we first need to understand which numbers can potentially be part of good subsets.
Numbers that can NEVER be in a good subset are those that already contain repeated prime factors:
- Numbers divisible by
4 = 2²
,9 = 3²
,25 = 5²
cannot be used - These numbers would immediately make any subset "bad"
Numbers that CAN be in good subsets:
- Square-free numbers (numbers with no repeated prime factors)
- The number 1 is special - it doesn't contribute any prime factors, so it can be freely added to any good subset
Since we're dealing with numbers up to 30, there are only 10 possible prime factors: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
. This small number of primes suggests we can use bitmask dynamic programming.
The core idea is to use a bitmask to represent which primes are "used" in our current subset's product:
- Each bit position represents one of the 10 primes
- If bit
i
is set to 1, it means primeprimes[i]
is used in our product - A valid good subset must never use the same prime twice
We can build up our answer using dynamic programming where f[mask]
represents the number of ways to form subsets that use exactly the primes indicated by the bitmask.
For each valid number x
(2 to 30, excluding those with repeated prime factors):
- Calculate its prime mask (which primes divide
x
) - For each existing state, if we can add
x
without repeating primes (no overlap in masks), we update the count
The special handling of 1s: Since any number of 1s can be added to any good subset without affecting its "goodness", if there are cnt[1]
ones in the array, each good subset can be combined with any subset of these 1s, giving us 2^cnt[1]
variations for each good subset.
Learn more about Math, Dynamic Programming and Bitmask patterns.
Solution Approach
The implementation uses bitmask dynamic programming to count all good subsets:
1. Initialize Constants and Data Structures:
- Define
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
- all primes up to 30 - Use
Counter
to count frequency of each number innums
- Create DP array
f
of size2^10
(1024) wheref[mask]
represents the number of ways to form subsets using exactly the primes indicated bymask
2. Base Case:
f[0] = pow(2, cnt[1])
- The empty prime set (no primes used) can be formed by taking any subset of 1s from the array. Since 1 doesn't contribute any prime factors, we have2^cnt[1]
ways.
3. Process Each Valid Number (2 to 30):
for x in range(2, 31):
if cnt[x] == 0 or x % 4 == 0 or x % 9 == 0 or x % 25 == 0:
continue
- Skip if the number doesn't appear in
nums
or contains repeated prime factors
4. Calculate Prime Mask for Current Number:
mask = 0
for i, p in enumerate(primes):
if x % p == 0:
mask |= 1 << i
- Create a bitmask representing which primes divide
x
- If prime
p
at indexi
dividesx
, set biti
in the mask
5. Update DP States:
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 states in reverse order (to avoid using updated values in the same iteration)
state & mask == mask
checks ifstate
contains all primes inmask
(i.e., we can removemask
fromstate
)state ^ mask
removes the primes ofx
from the current state, giving us the previous state- We add
cnt[x] * f[state ^ mask]
because we can choose any of thecnt[x]
occurrences ofx
to add to subsets represented byf[state ^ mask]
6. Calculate Final Answer:
return sum(f[i] for i in range(1, 1 << n)) % mod
- Sum all non-empty prime combinations (
f[1]
throughf[1023]
) - Exclude
f[0]
as it represents the empty subset (or subsets containing only 1s, which have no prime factors)
The algorithm efficiently counts all possible good subsets by tracking which primes have been used, ensuring no prime appears more than once in any subset's product.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with nums = [1, 2, 3, 4]
.
Step 1: Count frequencies
cnt[1] = 1, cnt[2] = 1, cnt[3] = 1, cnt[4] = 1
Step 2: Initialize DP array
f[0] = 2^1 = 2
(we have one1
in the array, so 2 ways: include it or not)- All other
f[mask] = 0
initially
Step 3: Process valid numbers (2 to 30)
Processing x = 2:
2
is valid (not divisible by 4, 9, or 25)- Prime mask:
2 = 2^1
, somask = 0b0000000001
(bit 0 set, representing prime 2) - Update states:
- For
state = 0b0000000001
(has prime 2):- Check:
state & mask == mask
? Yes (0b01 & 0b01 = 0b01) - Previous state:
state ^ mask = 0b01 ^ 0b01 = 0b00
- Update:
f[0b01] = (0 + 1 * f[0b00]) % mod = 1 * 2 = 2
- Check:
- For
Processing x = 3:
3
is valid- Prime mask:
3 = 3^1
, somask = 0b0000000010
(bit 1 set, representing prime 3) - Update states:
- For
state = 0b0000000011
(has primes 2 and 3):- Check:
state & mask == mask
? Yes (0b11 & 0b10 = 0b10) - Previous state:
state ^ mask = 0b11 ^ 0b10 = 0b01
- Update:
f[0b11] = (0 + 1 * f[0b01]) % mod = 1 * 2 = 2
- Check:
- For
state = 0b0000000010
(has prime 3):- Check:
state & mask == mask
? Yes (0b10 & 0b10 = 0b10) - Previous state:
state ^ mask = 0b10 ^ 0b10 = 0b00
- Update:
f[0b10] = (0 + 1 * f[0b00]) % mod = 1 * 2 = 2
- Check:
- For
Processing x = 4:
4 = 2²
is NOT valid (divisible by 4)- Skip this number
Step 4: Calculate final answer
- Sum all non-empty states:
f[0b01] + f[0b10] + f[0b11] = 2 + 2 + 2 = 6
Verification of the 6 good subsets:
[2]
→ product = 2 (prime factorization: 2¹) ✓[1, 2]
→ product = 2 (prime factorization: 2¹) ✓[3]
→ product = 3 (prime factorization: 3¹) ✓[1, 3]
→ product = 3 (prime factorization: 3¹) ✓[2, 3]
→ product = 6 (prime factorization: 2¹ × 3¹) ✓[1, 2, 3]
→ product = 6 (prime factorization: 2¹ × 3¹) ✓
Note that subsets containing 4
like [4]
, [1, 4]
, [2, 4]
, etc., are NOT good because 4 = 2²
has a repeated prime factor.
The DP approach efficiently counts these by tracking which primes are used (via bitmask) and ensuring no prime appears twice.
Solution Implementation
1class Solution:
2 def numberOfGoodSubsets(self, nums: List[int]) -> int:
3 # Define all prime numbers up to 30
4 primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
5
6 # Count frequency of each number in the input array
7 count = Counter(nums)
8
9 # Define modulo for the result
10 MOD = 10**9 + 7
11
12 # Number of prime factors we're tracking
13 num_primes = len(primes)
14
15 # dp[mask] represents the number of good subsets where mask indicates which primes are used
16 # If bit i is set in mask, it means primes[i] is a factor of some number in the subset
17 dp = [0] * (1 << num_primes)
18
19 # Base case: empty subset can be combined with any number of 1s
20 # Since 1 doesn't affect the product being square-free, we can choose any subset of 1s
21 dp[0] = pow(2, count[1], MOD)
22
23 # Iterate through all possible numbers from 2 to 30
24 for num in range(2, 31):
25 # Skip if this number doesn't appear in the input
26 if count[num] == 0:
27 continue
28
29 # Skip numbers that have a prime factor appearing more than once
30 # (i.e., numbers divisible by 4=2², 9=3², or 25=5²)
31 if num % 4 == 0 or num % 9 == 0 or num % 25 == 0:
32 continue
33
34 # Calculate the prime factorization mask for current number
35 prime_mask = 0
36 for i, prime in enumerate(primes):
37 if num % prime == 0:
38 prime_mask |= 1 << i
39
40 # Update dp states in reverse order to avoid using updated values
41 for state in range((1 << num_primes) - 1, 0, -1):
42 # Check if current state contains all prime factors of num
43 # This ensures no prime appears more than once in the product
44 if state & prime_mask == prime_mask:
45 # Add subsets that include current number to existing subsets
46 # state ^ prime_mask gives the state without primes from current number
47 dp[state] = (dp[state] + count[num] * dp[state ^ prime_mask]) % MOD
48
49 # Sum all non-empty subsets (exclude dp[0] which represents empty subset)
50 result = sum(dp[i] for i in range(1, 1 << num_primes)) % MOD
51
52 return result
53
1class Solution {
2 public int numberOfGoodSubsets(int[] nums) {
3 // Define all prime numbers up to 30
4 int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
5
6 // Count frequency of each number (1 to 30)
7 int[] frequency = new int[31];
8 for (int num : nums) {
9 frequency[num]++;
10 }
11
12 // Define modulo value for preventing overflow
13 final int MOD = (int) 1e9 + 7;
14 int primeCount = primes.length;
15
16 // Dynamic programming array
17 // dp[mask] represents the number of ways to form subsets using primes indicated by mask
18 long[] dp = new long[1 << primeCount];
19 dp[0] = 1; // Base case: empty subset
20
21 // Handle 1s separately - each 1 can either be included or excluded
22 // This multiplies the count by 2^(count of 1s)
23 for (int i = 0; i < frequency[1]; i++) {
24 dp[0] = (dp[0] * 2) % MOD;
25 }
26
27 // Process each number from 2 to 30
28 for (int num = 2; num <= 30; num++) {
29 // Skip if number doesn't appear or has square factors (not square-free)
30 if (frequency[num] == 0 || num % 4 == 0 || num % 9 == 0 || num % 25 == 0) {
31 continue;
32 }
33
34 // Create bitmask representing which primes divide this number
35 int primeMask = 0;
36 for (int i = 0; i < primeCount; i++) {
37 if (num % primes[i] == 0) {
38 primeMask |= 1 << i;
39 }
40 }
41
42 // Update dp states in reverse order to avoid using updated values
43 for (int state = (1 << primeCount) - 1; state > 0; state--) {
44 // Check if current state contains all primes needed for this number
45 if ((state & primeMask) == primeMask) {
46 // Add ways to form current state by including this number
47 // Previous state is obtained by removing primes of current number
48 dp[state] = (dp[state] + frequency[num] * dp[state ^ primeMask]) % MOD;
49 }
50 }
51 }
52
53 // Calculate total number of good subsets (exclude empty subset)
54 long result = 0;
55 for (int i = 1; i < (1 << primeCount); i++) {
56 result = (result + dp[i]) % MOD;
57 }
58
59 return (int) result;
60 }
61}
62
1class Solution {
2public:
3 int numberOfGoodSubsets(vector<int>& nums) {
4 // Define the first 10 prime numbers (all primes up to 30)
5 int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
6
7 // Count frequency of each number in nums (numbers are between 1-30)
8 int frequency[31] = {0};
9 for (int& num : nums) {
10 ++frequency[num];
11 }
12
13 int primeCount = 10;
14 const int MOD = 1e9 + 7;
15
16 // dp[mask] = number of ways to form subsets using primes indicated by mask
17 // mask is a bitmask where bit i represents whether prime[i] is used
18 vector<long long> dp(1 << primeCount);
19 dp[0] = 1;
20
21 // Handle number 1 specially: each 1 can either be included or not
22 // This multiplies the count by 2^(count of 1s)
23 for (int i = 0; i < frequency[1]; ++i) {
24 dp[0] = dp[0] * 2 % MOD;
25 }
26
27 // Process each number from 2 to 30
28 for (int number = 2; number <= 30; ++number) {
29 // Skip if number doesn't appear in nums
30 if (frequency[number] == 0) {
31 continue;
32 }
33
34 // Skip numbers with repeated prime factors (not square-free)
35 // Numbers divisible by 4=2², 9=3², or 25=5² have repeated prime factors
36 if (number % 4 == 0 || number % 9 == 0 || number % 25 == 0) {
37 continue;
38 }
39
40 // Create bitmask representing which primes divide this number
41 int primeMask = 0;
42 for (int i = 0; i < primeCount; ++i) {
43 if (number % primes[i] == 0) {
44 primeMask |= (1 << i);
45 }
46 }
47
48 // Update dp states in reverse order to avoid using updated values
49 for (int state = (1 << primeCount) - 1; state > 0; --state) {
50 // Check if current state contains all primes needed for this number
51 if ((state & primeMask) == primeMask) {
52 // Add ways to form state by adding current number to state^primeMask
53 // state^primeMask removes the primes used by current number
54 dp[state] = (dp[state] + 1LL * frequency[number] * dp[state ^ primeMask]) % MOD;
55 }
56 }
57 }
58
59 // Sum up all non-empty subsets (exclude dp[0])
60 long long result = 0;
61 for (int mask = 1; mask < (1 << primeCount); ++mask) {
62 result = (result + dp[mask]) % MOD;
63 }
64
65 return result;
66 }
67};
68
1function numberOfGoodSubsets(nums: number[]): number {
2 // Define the first 10 prime numbers (all primes up to 30)
3 const primes: number[] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
4
5 // Count frequency of each number in nums (numbers are between 1-30)
6 const frequency: number[] = new Array(31).fill(0);
7 for (const num of nums) {
8 frequency[num]++;
9 }
10
11 const primeCount: number = 10;
12 const MOD: number = 1e9 + 7;
13
14 // dp[mask] = number of ways to form subsets using primes indicated by mask
15 // mask is a bitmask where bit i represents whether prime[i] is used
16 const dp: number[] = new Array(1 << primeCount).fill(0);
17 dp[0] = 1;
18
19 // Handle number 1 specially: each 1 can either be included or not
20 // This multiplies the count by 2^(count of 1s)
21 for (let i = 0; i < frequency[1]; i++) {
22 dp[0] = (dp[0] * 2) % MOD;
23 }
24
25 // Process each number from 2 to 30
26 for (let number = 2; number <= 30; number++) {
27 // Skip if number doesn't appear in nums
28 if (frequency[number] === 0) {
29 continue;
30 }
31
32 // Skip numbers with repeated prime factors (not square-free)
33 // Numbers divisible by 4=2², 9=3², or 25=5² have repeated prime factors
34 if (number % 4 === 0 || number % 9 === 0 || number % 25 === 0) {
35 continue;
36 }
37
38 // Create bitmask representing which primes divide this number
39 let primeMask: number = 0;
40 for (let i = 0; i < primeCount; i++) {
41 if (number % primes[i] === 0) {
42 primeMask |= (1 << i);
43 }
44 }
45
46 // Update dp states in reverse order to avoid using updated values
47 for (let state = (1 << primeCount) - 1; state > 0; state--) {
48 // Check if current state contains all primes needed for this number
49 if ((state & primeMask) === primeMask) {
50 // Add ways to form state by adding current number to state^primeMask
51 // state^primeMask removes the primes used by current number
52 dp[state] = (dp[state] + frequency[number] * dp[state ^ primeMask]) % MOD;
53 }
54 }
55 }
56
57 // Sum up all non-empty subsets (exclude dp[0])
58 let result: number = 0;
59 for (let mask = 1; mask < (1 << primeCount); mask++) {
60 result = (result + dp[mask]) % MOD;
61 }
62
63 return result;
64}
65
Time and Space Complexity
Time Complexity: O(2^n * (n + 30))
where n = 10
(number of primes)
- Counting frequency of numbers in
nums
takesO(len(nums))
time - Computing
pow(2, cnt[1])
takesO(log(cnt[1]))
time - The outer loop iterates through numbers 2 to 30, which is
O(30)
iterations - For each valid number
x
, we compute its prime mask inO(n)
time by checking divisibility with each prime - The inner loop iterates through all states from
(1 << n) - 1
to 1, which isO(2^n)
iterations - Each state update operation takes
O(1)
time - The final summation iterates through
2^n - 1
states - Since
n = 10
is a constant, the overall time complexity isO(2^10 * 40) = O(1024 * 40)
which simplifies toO(1)
for constant bounds, but more preciselyO(2^n * n)
for the algorithmic analysis
Space Complexity: O(2^n)
where n = 10
- The
primes
array usesO(n)
space - The
cnt
Counter usesO(30)
space at most (for numbers 1-30) - The
f
array usesO(2^n)
space to store DP states for all possible subsets of primes - Since
n = 10
, the space complexity isO(2^10) = O(1024)
, which is effectivelyO(1)
for constant bounds, but more preciselyO(2^n)
for the algorithmic analysis
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of the Number 1
Pitfall: A common mistake is treating 1 like any other number when updating the DP states. Some might try to process 1 in the main loop along with numbers 2-30, which would incorrectly update the DP states since 1 has no prime factors.
Why it's wrong: The number 1 is special because:
- It has no prime factors (empty prime factorization)
- It can be freely added to any good subset without affecting whether the subset remains good
- Multiple 1s can be included without violating the square-free property
Correct approach:
# Handle 1s separately in the base case
dp[0] = pow(2, count[1], MOD) # 2^count[1] ways to choose 1s
# Then process numbers 2-30 in the main loop
for num in range(2, 31):
# ... process numbers with actual prime factors
2. Not Checking for Numbers with Repeated Prime Factors
Pitfall: Forgetting to skip numbers that inherently have repeated prime factors (like 4, 8, 9, 12, 16, 18, 20, 24, 25, 27, 28).
Why it's wrong: These numbers can never be part of a good subset because their prime factorization already contains repeated primes:
- 4 = 2² (prime 2 appears twice)
- 9 = 3² (prime 3 appears twice)
- 12 = 2² × 3 (prime 2 appears twice)
Correct approach:
# Must check for all perfect square factors up to 30 if num % 4 == 0 or num % 9 == 0 or num % 25 == 0: continue # Skip numbers with repeated prime factors
3. Updating DP States in Wrong Order
Pitfall: Updating DP states in forward order (from 0 to 2^n - 1) instead of reverse order.
Why it's wrong: If we update in forward order, we might use already-updated values from the current iteration when calculating later states, leading to incorrect counting (elements being counted multiple times).
Correct approach:
# Update in reverse order to ensure we use values from previous iteration
for state in range((1 << num_primes) - 1, 0, -1):
if state & prime_mask == prime_mask:
dp[state] = (dp[state] + count[num] * dp[state ^ prime_mask]) % MOD
4. Incorrectly Checking Prime Mask Compatibility
Pitfall: Using state & prime_mask == 0
to check if a number can be added to a state, or using state | prime_mask
incorrectly.
Why it's wrong:
state & prime_mask == 0
would check if there's NO overlap, but we're updating states that already contain the primes- We need to check if the current state can have the prime_mask "removed" from it
Correct approach:
# Check if state contains all primes in prime_mask if state & prime_mask == prime_mask: # state ^ prime_mask removes those primes from state dp[state] = (dp[state] + count[num] * dp[state ^ prime_mask]) % MOD
5. Forgetting to Apply Modulo Operations
Pitfall: Not applying modulo at every arithmetic operation, especially when calculating powers or sums.
Why it's wrong: The numbers can grow very large, causing integer overflow. Even in Python which handles big integers, not using modulo can slow down calculations significantly.
Correct approach:
# Use modulo in pow function
dp[0] = pow(2, count[1], MOD) # Third parameter applies modulo efficiently
# Apply modulo after each addition
dp[state] = (dp[state] + count[num] * dp[state ^ prime_mask]) % MOD
# Apply modulo to final sum
result = sum(dp[i] for i in range(1, 1 << num_primes)) % MOD
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!