Skip to content
← All posts

The Number of Good Subsets: Bitmask DP Over Primes

6 min read
leetcodeproblemhardarraysmathdynamic-programmingbit-manipulation

LeetCode The Number of Good Subsets (Problem 1994) gives you an integer array nums. A good subset is a non-empty subset of nums where the product of its elements can be represented as a product of one or more distinct prime numbers. Return the number of different good subsets, modulo 10^9 + 7.

For example, given nums = [1, 2, 3, 4], the good subsets are [1, 2], [1, 3], [1, 2, 3], [2], [3], and [2, 3]. The subset [4] is not good because 4 = 2 * 2, and the prime factor 2 repeats. The answer is 6.

The constraints say 1 <= nums[i] <= 30. That cap is the entire trick.

Numbers 1 to 30: valid vs invalidGreen = product of distinct primes, Red = has squared prime factor1special22334invalid5562×3778invalid9invalid102×5111112invalid1313142×7153×516invalid171718invalid191920invalid213×7222×11232324invalid25invalid262×1327invalid28invalid2929302×3×5Prime bitmasks (valid numbers only)23571113171923292= 200000000013= 300000000105= 500000001006= 2×300000000117= 7000000100010= 2×5000000010111= 11000001000013= 13000010000014= 2×7000000100115= 3×5000000011017= 17000100000019= 19001000000021= 3×7000000101022= 2×11000001000123= 23010000000026= 2×13000010000129= 29100000000030= 2×3×50000000111
Numbers 1 to 30 classified by their prime factorization. Only numbers whose prime factors are all distinct (square-free) can appear in a good subset. Each valid number maps to a bitmask over the 10 primes up to 30.

Why this problem matters

This problem sits at the intersection of number theory and bitmask DP. You need to recognize that the small value range (1 to 30) means only 10 primes are relevant, and those 10 primes fit neatly into a 10-bit bitmask. That transforms a combinatorial explosion into a manageable state space. Problems that combine prime factorization with bitmask enumeration appear often in competitive programming, and the pattern transfers directly to other "subset with constraints" problems.

Key insight: prime bitmasks

There are exactly 10 primes up to 30: 2, 3, 5, 7, 11, 13, 17, 19, 23, and 29. Since every number in the array is at most 30, its prime factorization only involves these 10 primes.

A good subset requires no repeated prime factors in the product. That means:

  1. Invalid numbers: Any number with a squared prime factor (like 4 = 2 x 2, 8 = 2 x 2 x 2, 9 = 3 x 3, 12, 16, 18, 20, 24, 25, 27, 28) can never appear in a good subset. Skip them entirely.

  2. Valid numbers: Numbers like 2, 3, 5, 6 (= 2 x 3), 7, 10 (= 2 x 5), etc. have all distinct prime factors. Represent each valid number as a bitmask over the 10 primes.

  3. The number 1 is special: It has no prime factors at all. Including any number of 1s in a subset never creates a repeated prime. Handle 1s separately by baking pow(2, count_of_ones) into the DP base case.

Since we only need 10 bits, the total state space is 2^10 = 1024 masks. That is tiny.

The approach

Define dp[mask] as the number of ways to form a subset of the processed numbers whose product has exactly the set of prime factors indicated by mask.

Base case: dp[0] = pow(2, count_of_ones). This accounts for every possible combination of 1s that can be included alongside any subset. Each copy of 1 can independently be present or absent without affecting prime factors.

Transition: For each valid number v (from 2 to 30) with prime mask m and count c in the array, iterate over all existing masks. If existing & m == 0 (no overlapping primes), then dp[existing | m] += dp[existing] * c.

Answer: Sum dp[mask] for all mask > 0. The 1s are already baked into the base case, so no separate multiplication is needed.

Clean Python solution

def numberOfGoodSubsets(nums):
    MOD = 10**9 + 7
    primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
    cnt = [0] * 31
    for x in nums:
        cnt[x] += 1

    def prime_mask(n):
        mask = 0
        for i, p in enumerate(primes):
            if n % p == 0:
                n //= p
                if n % p == 0:
                    return -1
                mask |= 1 << i
        return mask

    dp = [0] * 1024
    dp[0] = pow(2, cnt[1], MOD)

    for v in range(2, 31):
        if cnt[v] == 0:
            continue
        m = prime_mask(v)
        if m < 0:
            continue
        for mask in range(1023, -1, -1):
            if dp[mask] and (mask & m) == 0:
                dp[mask | m] = (dp[mask | m] + dp[mask] * cnt[v]) % MOD

    return sum(dp[1:]) % MOD

Step-by-step walkthrough

Let's trace the DP with a small example: nums = [2, 3, 6, 5]. The valid numbers are 2 (mask bit 0), 3 (mask bit 1), 6 = 2 x 3 (mask bits 0 and 1), and 5 (mask bit 2).

Step 1: Initialize dp[0] = 1 (empty subset)

dp[mask] = number of subsets whose product has exactly those prime factorsmask=01mask=1..10230All 1024 masks start at 0, except the empty mask

The base case: there is exactly 1 way to form a product with no prime factors (the empty subset).

Step 2: Process number 2 (prime mask = bit 0)

Number 2 has mask 0b0000000001. Find all existing masks where bit 0 is NOT set.dp[0]=10000000000bit 0 free? Yesnew dp[1]0000000001dp[0|1] += dp[0]dp[0]=1, dp[1]=1. One way to get prime 2 in the product.

For each existing mask that does not overlap with 2's mask, we add dp[existing] to dp[existing | mask(2)].

Step 3: Process number 3 (prime mask = bit 1)

Number 3 has mask 0b0000000010. Check existing masks with bit 1 clear.dp[0]=100000000000 | 2 = 2dp[1]=100000000011 | 2 = 3dp[2]=10000000010dp[3]=10000000011dp[2]=1 (subset {3}), dp[3]=1 (subset {2,3})

Now dp[0]=1, dp[1]=1, dp[2]=1, dp[3]=1. Four masks are populated.

Step 4: Process number 6 = 2 x 3 (prime mask = bits 0,1 = 0b11)

Number 6 has mask 0b0000000011. Needs BOTH bits 0 and 1 clear in existing mask.dp[0]=100000000000 & 3 = 0, OKdp[1]=100000000011 & 3 = 1, overlap!dp[2]=100000000102 & 3 = 2, overlap!dp[3]+=10000000011dp[3] = 2 now

Only dp[0] has no overlap with 6's mask. dp[3] goes from 1 to 2, because both {2,3} and {6} produce primes 2 and 3.

Step 5: Process number 5 (prime mask = bit 2)

Number 5 has mask 0b0000000100. Check all 4 populated masks for bit 2 clear.dp[0]=100000000000|4 = 4dp[1]=100000000011|4 = 5dp[2]=100000000102|4 = 6dp[3]=200000000113|4 = 7dp[4]=1, dp[5]=1, dp[6]=1, dp[7]=2. Total good subsets = 1+1+1+1+2+1+1+2 = 10

Bit 2 does not conflict with any existing mask, so 5 combines with every populated state. The answer sums all dp[mask] for mask > 0.

Step 6: Account for 1s via the base case

Each 1 in the array can independently be included or excluded from any good subset.Set dp[0] = pow(2, count_of_ones) before the DP loop. This bakes everycombination of 1s into each mask transition automatically.answer = sum of dp[mask] for mask > 0, mod (10^9 + 7)

The number 1 has no prime factors, so including it never creates a repeated prime. With k copies of 1, dp[0] starts at 2^k, propagating through all transitions.

The key moment is Step 4, where number 6 (with mask 0b11) can only combine with masks that have both bits 0 and 1 clear. Only the empty mask qualifies, so 6 contributes to dp[3] but cannot pair with subsets already containing 2 or 3. This is exactly the "no overlapping primes" constraint enforced by the bitwise AND check.

Complexity analysis

AspectValue
TimeO(n + 30 * 2^10)
SpaceO(2^10)

Building the frequency count takes O(n). The DP iterates over at most 29 valid numbers (2 to 30), and for each one scans all 1024 masks. That gives 29 * 1024 = roughly 30,000 operations, which is constant. The dp array has 1024 entries. Total space is O(1024) = O(1) beyond the input.

The building blocks

Bitmask DP. Representing a set of chosen elements as a bitmask and using mask & new == 0 to check compatibility is a fundamental technique. You see it in traveling salesman, set cover, and any problem where you need to track "which items have been used" with a small universe.

Prime factorization. Breaking each number into its prime factors and checking for squared factors is the preprocessing step that classifies numbers as valid or invalid. The small range (1 to 30) makes trial division perfectly efficient.

Modular arithmetic. The answer can be astronomically large, so you work modulo 10^9 + 7 throughout. The key operation is pow(2, cnt[1], MOD) for the base case, which uses modular exponentiation to handle potentially millions of 1s efficiently.

Edge cases

All 1s. If the array contains only 1s, the answer is 0. The problem requires the product to be a product of one or more distinct primes. A product of 1 does not qualify because it contains zero primes. No valid numbers (2 to 30) exist, so the DP loop does nothing and sum(dp[1:]) returns 0.

Single element. If the array has one valid number (like 2, 3, 5, 6, etc.), the answer is 1. If the only element is 1 or an invalid number (like 4), the answer is 0.

All invalid numbers. If every element is something like 4, 8, 9, 16, etc., no good subset exists. The answer is 0.

Large count of the same number. If you have 100,000 copies of 2, you can include at most one of them in a good subset (because 2 x 2 repeats the prime 2). The count cnt[2] = 100000 means there are 100,000 different ways to pick that single 2. The DP transition dp[mask | m] += dp[mask] * cnt[v] accounts for this.

From understanding to recall

The trick to this problem is seeing the small prime universe and mapping it to bitmask DP. Once you spot that there are only 10 primes up to 30, the solution structure falls into place. But recognizing that connection under interview pressure takes practice.

Spaced repetition helps you internalize the pattern. You solve the problem today, see it again in three days, then a week later. Each repetition reinforces the link between "small value range," "prime bitmask," and "DP over masks." Eventually, when you see a problem with constrained values and subset selection, the bitmask DP approach surfaces automatically.

Related posts

  • Counting Bits - Bit manipulation fundamentals that help with bitmask DP
  • Partition Equal Subset Sum - Another subset problem solved with DP, different approach but similar thinking
  • Combination Sum - Subset selection with constraints, showing how DP handles combinatorial choices