2572. Count the Number of Square-Free Subsets
Problem Description
You are given a positive integer array nums
(0-indexed).
A square-free integer is an integer that is not divisible by any perfect square other than 1. In other words, it cannot be divided by numbers like 4, 9, 16, 25, etc.
A square-free subset is a subset of nums
where the product of all its elements results in a square-free integer.
Your task is to find the total number of non-empty square-free subsets that can be formed from the array nums
. Since the answer can be very large, return it modulo 10^9 + 7
.
Key points to understand:
- A subset is formed by selecting some (possibly all, but not none) elements from
nums
- Two subsets are considered different if they are formed by selecting different indices from the array
- The product of elements in a valid subset must be square-free
- You need to count all possible non-empty subsets that satisfy the square-free condition
For example, if we have a subset {2, 3, 5}
, their product is 2 Ă— 3 Ă— 5 = 30
, which is square-free since it's not divisible by any perfect square except 1. However, if we have {2, 2}
, their product is 4
, which is not square-free since it's divisible by the perfect square 4.
Intuition
Let's think about what makes a number square-free. A number is square-free if no prime appears more than once in its prime factorization. For example, 30 = 2 Ă— 3 Ă— 5
is square-free, but 12 = 2² × 3
is not because 2 appears twice.
Since the problem limits nums[i]
to the range [1, 30]
, we can identify all prime numbers up to 30: [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
. This gives us only 10 primes to work with.
The key insight is that for a product of multiple numbers to be square-free, each prime factor across all numbers can appear at most once in total. If we multiply two numbers that share a common prime factor, the product won't be square-free. For instance, multiplying 6 = 2 Ă— 3
with 10 = 2 Ă— 5
gives 60 = 2² × 3 × 5
, which isn't square-free because 2 appears twice.
This constraint suggests we can represent which primes are "used" in our subset using a bitmask. With 10 primes, we need 10 bits, where bit i
indicates whether the i
-th prime is present in our product. This gives us 2^10 = 1024
possible states.
We can use dynamic programming where f[state]
represents the number of ways to form subsets whose product has exactly the prime factors indicated by the bitmask state
.
For each number x
in the range [2, 30]
:
- First, check if
x
itself is square-free (skip numbers like 4, 9, 25 which contain squared primes) - Find which primes divide
x
and represent this as a bitmask - For each existing state, if we can add
x
to subsets with that state (meaning no prime conflicts), we update the count
The number 1
is special - it doesn't contribute any prime factors, so we can include any number of 1's in any subset. If there are cnt[1]
ones in the array, we can choose to include 0, 1, 2, ..., or all of them, giving us 2^cnt[1]
choices for each valid subset configuration.
Learn more about Math, Dynamic Programming and Bitmask patterns.
Solution Approach
We implement the solution using state compression dynamic programming with bitmasks to track which prime factors are used.
Step 1: Preprocessing
- Define the list of primes up to 30:
primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
- Count the frequency of each number in
nums
usingCounter
- Initialize DP array
f
of size2^10
wheref[state]
represents the number of ways to form subsets with prime factors represented by the bitmaskstate
Step 2: Handle the special case of 1
- Set
f[0] = 2^cnt[1]
because we can choose any subset of 1's (including empty subset) - The number 1 has no prime factors, so it only affects the empty state
Step 3: Process each number from 2 to 30
For each number x
in range [2, 30]
:
-
Skip invalid numbers:
- Skip if
cnt[x] == 0
(number not in array) - Skip if
x
is divisible by perfect squares:x % 4 == 0
orx % 9 == 0
orx % 25 == 0
- Skip if
-
Calculate the prime mask for x:
mask = 0 for i, p in enumerate(primes): if x % p == 0: mask |= 1 << i
This creates a bitmask where bit
i
is set if primeprimes[i]
dividesx
. -
Update states in reverse order:
for state in range((1 << n) - 1, 0, -1): if state & mask == mask: f[state] = (f[state] + cnt[x] * f[state ^ mask]) % mod
- We iterate states from large to small to avoid using updated values in the same iteration
- Condition
state & mask == mask
checks ifstate
contains all primes inmask
state ^ mask
removes the primes ofx
fromstate
, giving us the previous state- We multiply by
cnt[x]
because we can choose any occurrence ofx
from the array
Step 4: Calculate the final answer
- Sum all values in
f
: this gives the total number of square-free subsets (including empty subset from choosing no elements except possibly 1's) - Subtract 1 to exclude the empty subset:
sum(f) % mod - 1
The time complexity is O(n + C Ă— M)
where n
is the length of nums
, C = 30
is the range of values, and M = 2^10
is the number of states. The space complexity is O(M)
for the DP array.
Ready to land your dream job?
Unlock your dream job with a 5-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through a small example with nums = [2, 3, 6]
.
Step 1: Identify key information
- Primes up to 30:
[2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
- Count frequencies:
cnt[2] = 1
,cnt[3] = 1
,cnt[6] = 1
- We need 10 bits for our bitmask (one for each prime)
Step 2: Initialize DP array
f[0] = 1
(since there are no 1's in our array, we start with 1 way to form empty product)- All other states start at 0
Step 3: Process each number
Processing number 2:
- 2 is prime and square-free
- Prime mask for 2:
mask = 0b0000000001
(only bit 0 set, representing prime 2) - Update states in reverse:
- For
state = 0b0000000001
(has prime 2):- Check:
state & mask == mask
? Yes (0b0000000001 & 0b0000000001 = 0b0000000001) - Previous state:
state ^ mask = 0b0000000000
- Update:
f[0b0000000001] = f[0b0000000001] + 1 * f[0b0000000000] = 0 + 1 * 1 = 1
- Check:
- For
Processing number 3:
- 3 is prime and square-free
- Prime mask for 3:
mask = 0b0000000010
(bit 1 set, representing prime 3) - Update states in reverse:
- For
state = 0b0000000011
(has primes 2 and 3):- Check:
state & mask == mask
? Yes - Previous state:
state ^ mask = 0b0000000001
- Update:
f[0b0000000011] = 0 + 1 * f[0b0000000001] = 0 + 1 * 1 = 1
- Check:
- For
state = 0b0000000010
(has prime 3):- Check:
state & mask == mask
? Yes - Previous state:
state ^ mask = 0b0000000000
- Update:
f[0b0000000010] = 0 + 1 * f[0b0000000000] = 0 + 1 * 1 = 1
- Check:
- For
Processing number 6:
- 6 = 2 Ă— 3, so it's square-free
- Prime mask for 6:
mask = 0b0000000011
(bits 0 and 1 set, for primes 2 and 3) - Update states in reverse:
- For
state = 0b0000000011
(has primes 2 and 3):- Check:
state & mask == mask
? Yes - Previous state:
state ^ mask = 0b0000000000
- Update:
f[0b0000000011] = 1 + 1 * f[0b0000000000] = 1 + 1 * 1 = 2
- Check:
- For
Step 4: Calculate final answer
Current DP state:
f[0b0000000000] = 1
(empty subset)f[0b0000000001] = 1
(subset {2}, product = 2)f[0b0000000010] = 1
(subset {3}, product = 3)f[0b0000000011] = 2
(subsets {2,3} with product = 6, and {6} with product = 6)
Total = 1 + 1 + 1 + 2 = 5 Answer = 5 - 1 = 4 (excluding empty subset)
The 4 valid square-free subsets are:
{2}
→ product = 2 (square-free){3}
→ product = 3 (square-free){6}
→ product = 6 (square-free){2, 3}
→ product = 6 (square-free)
Note that {2, 6}
would give product = 12 = 2² × 3 (not square-free), and {3, 6}
would give product = 18 = 2 × 3² (not square-free), so these are correctly not counted by our algorithm.
Solution Implementation
1class Solution:
2 def squareFreeSubsets(self, nums: List[int]) -> int:
3 # List of prime numbers up to 29 (since max number is 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 frequency = Counter(nums)
8
9 # Modulo for the answer
10 MOD = 10**9 + 7
11
12 # Number of prime factors we're tracking
13 num_primes = len(primes)
14
15 # dp[mask] = number of ways to form subsets with prime factors represented by mask
16 # mask is a bitmask where bit i represents whether prime[i] is a factor
17 dp = [0] * (1 << num_primes)
18
19 # Base case: empty subset can be combined with any subset of 1's
20 # Since 1 has no prime factors, we can choose any subset of all 1's
21 dp[0] = pow(2, frequency[1], MOD)
22
23 # Process each number from 2 to 30
24 for number in range(2, 31):
25 # Skip if number doesn't appear in input
26 if frequency[number] == 0:
27 continue
28
29 # Skip if number contains a square factor (not square-free)
30 if number % 4 == 0 or number % 9 == 0 or number % 25 == 0:
31 continue
32
33 # Calculate the prime factorization mask for this number
34 prime_mask = 0
35 for index, prime in enumerate(primes):
36 if number % prime == 0:
37 prime_mask |= 1 << index
38
39 # Update dp states that can include this number
40 # Iterate from largest state to smallest 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 the number
43 if state & prime_mask == prime_mask:
44 # Add contribution: frequency[number] * ways without this number
45 # state ^ prime_mask removes the prime factors of current number
46 dp[state] = (dp[state] + frequency[number] * dp[state ^ prime_mask]) % MOD
47
48 # Sum all possible subsets and subtract 1 for the empty subset
49 total = sum(dp) % MOD
50 return total - 1
51
1class Solution {
2 public int squareFreeSubsets(int[] nums) {
3 // First 10 prime numbers (all primes 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 final int MOD = 1000000007;
13 int primeCount = primes.length;
14
15 // dp[mask] represents the number of ways to form square-free subsets
16 // where mask indicates which primes have been used
17 long[] dp = new long[1 << primeCount];
18 dp[0] = 1; // Base case: empty subset
19
20 // Handle all 1s separately (can choose any subset of 1s)
21 // Each 1 can either be included or excluded, giving 2^count possibilities
22 for (int i = 0; i < frequency[1]; i++) {
23 dp[0] = (dp[0] * 2) % MOD;
24 }
25
26 // Process numbers from 2 to 30
27 for (int number = 2; number <= 30; number++) {
28 // Skip if number doesn't appear in input or contains square factors
29 if (frequency[number] == 0 ||
30 number % 4 == 0 ||
31 number % 9 == 0 ||
32 number % 25 == 0) {
33 continue;
34 }
35
36 // Calculate prime factorization mask for current number
37 int primeMask = 0;
38 for (int i = 0; i < primeCount; i++) {
39 if (number % primes[i] == 0) {
40 primeMask |= (1 << i);
41 }
42 }
43
44 // Update dp states in reverse order to avoid using updated values
45 for (int state = (1 << primeCount) - 1; state > 0; state--) {
46 // Check if current state contains all primes needed for this number
47 if ((state & primeMask) == primeMask) {
48 // Add ways to form this state by including current number
49 // Previous state is obtained by removing primes used by current number
50 dp[state] = (dp[state] + frequency[number] * dp[state ^ primeMask]) % MOD;
51 }
52 }
53 }
54
55 // Sum all possible square-free subsets
56 long totalWays = 0;
57 for (int state = 0; state < (1 << primeCount); state++) {
58 totalWays = (totalWays + dp[state]) % MOD;
59 }
60
61 // Subtract 1 to exclude the empty subset
62 totalWays--;
63
64 return (int) totalWays;
65 }
66}
67
1class Solution {
2public:
3 int squareFreeSubsets(vector<int>& nums) {
4 // List of prime numbers up to 29 (since max value is 30)
5 int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
6
7 // Count frequency of each number in nums (values range from 1 to 30)
8 int frequency[31] = {};
9 for (int& num : nums) {
10 ++frequency[num];
11 }
12
13 const int numPrimes = 10;
14 const int MOD = 1e9 + 7;
15
16 // dp[mask] = number of ways to form square-free subsets using prime factors represented by mask
17 // mask is a bitmask where bit i represents whether prime[i] is used
18 vector<long long> dp(1 << numPrimes);
19 dp[0] = 1; // Empty subset
20
21 // Handle 1's separately - each 1 can either be included or excluded
22 // This multiplies the count by 2^(count of 1's)
23 for (int i = 0; i < frequency[1]; ++i) {
24 dp[0] = dp[0] * 2 % MOD;
25 }
26
27 // Process numbers from 2 to 30
28 for (int num = 2; num <= 30; ++num) {
29 // Skip if number doesn't appear or contains square factors
30 if (frequency[num] == 0 || num % 4 == 0 || num % 9 == 0 || num % 25 == 0) {
31 continue;
32 }
33
34 // Build bitmask representing prime factors of current number
35 int primeMask = 0;
36 for (int i = 0; i < numPrimes; ++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 << numPrimes) - 1; state > 0; --state) {
44 // Check if current state contains all prime factors of num
45 if ((state & primeMask) == primeMask) {
46 // Add ways to form this state by including current number
47 // Previous state is obtained by removing prime factors of num
48 dp[state] = (dp[state] + 1LL * frequency[num] * dp[state ^ primeMask]) % MOD;
49 }
50 }
51 }
52
53 // Sum all possible states to get total number of square-free subsets
54 // Subtract 1 to exclude empty subset
55 long long result = -1;
56 for (int i = 0; i < (1 << numPrimes); ++i) {
57 result = (result + dp[i]) % MOD;
58 }
59
60 return result;
61 }
62};
63
1/**
2 * Counts the number of non-empty square-free subsets from the given array.
3 * A square-free integer is one that is not divisible by any perfect square other than 1.
4 *
5 * @param nums - Array of positive integers (1 <= nums[i] <= 30)
6 * @returns The count of square-free subsets modulo 10^9 + 7
7 */
8function squareFreeSubsets(nums: number[]): number {
9 // All prime numbers up to 30
10 const PRIMES: number[] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
11
12 // Count frequency of each number from 1 to 30
13 const frequencyCount: number[] = Array(31).fill(0);
14 for (const num of nums) {
15 frequencyCount[num]++;
16 }
17
18 const MOD: number = 1_000_000_007;
19 const primeCount: number = PRIMES.length;
20
21 // Dynamic programming array where dp[mask] represents the number of ways
22 // to form subsets using primes indicated by the bitmask
23 const dp: number[] = Array(1 << primeCount).fill(0);
24 dp[0] = 1;
25
26 // Handle the special case of 1: each 1 can either be included or excluded
27 // So multiply by 2 for each occurrence of 1
28 for (let i = 0; i < frequencyCount[1]; i++) {
29 dp[0] = (dp[0] * 2) % MOD;
30 }
31
32 // Process each number from 2 to 30
33 for (let currentNum = 2; currentNum <= 30; currentNum++) {
34 // Skip if number doesn't appear in input or if it contains perfect squares
35 if (frequencyCount[currentNum] === 0 ||
36 currentNum % 4 === 0 ||
37 currentNum % 9 === 0 ||
38 currentNum % 25 === 0) {
39 continue;
40 }
41
42 // Create bitmask representing which primes divide the current number
43 let primeMask: number = 0;
44 for (let primeIdx = 0; primeIdx < primeCount; primeIdx++) {
45 if (currentNum % PRIMES[primeIdx] === 0) {
46 primeMask |= (1 << primeIdx);
47 }
48 }
49
50 // Update dp states in reverse order to avoid using updated values
51 for (let state = (1 << primeCount) - 1; state > 0; state--) {
52 // Check if current state can accommodate the prime factors of currentNum
53 if ((state & primeMask) === primeMask) {
54 // Add ways to form the subset by including currentNum
55 const previousState = state ^ primeMask;
56 dp[state] = (dp[state] + frequencyCount[currentNum] * dp[previousState]) % MOD;
57 }
58 }
59 }
60
61 // Sum all possible subset configurations
62 let totalSubsets: number = 0;
63 for (let state = 0; state < (1 << primeCount); state++) {
64 totalSubsets = (totalSubsets + dp[state]) % MOD;
65 }
66
67 // Subtract 1 to exclude the empty subset
68 totalSubsets--;
69
70 // Handle negative result due to modulo operation
71 return totalSubsets >= 0 ? totalSubsets : totalSubsets + MOD;
72}
73
Time and Space Complexity
Time Complexity: O(2^n + 31 * 2^n)
where n = 10
(number of primes)
- Creating the counter takes
O(m)
wherem
is the length of the input arraynums
- Initializing array
f
takesO(2^n)
wheren = 10
- The main loop iterates through numbers 2 to 30 (29 iterations)
- For each valid number
x
, we check divisibility by 10 primes:O(10)
- For each valid number, we iterate through all states from
2^n - 1
to 1:O(2^n)
- Inside the inner loop, we perform constant time operations
- Total:
O(m + 2^n + 29 * (10 + 2^n))
=O(m + 2^n * 29)
=O(m + 2^10 * 29)
- Since
2^10 = 1024
and this dominates for typical input sizes, the complexity simplifies toO(2^n)
wheren = 10
Space Complexity: O(2^n + m)
where n = 10
and m
is the number of unique elements in nums
- The
f
array stores2^n = 2^10 = 1024
elements:O(2^n)
- The counter stores at most
m
unique elements from the input:O(m)
- The
primes
list is constant size:O(1)
- Other variables use constant space:
O(1)
- Total:
O(2^n + m)
=O(2^10 + m)
=O(1024 + m)
Learn more about how to find time and space complexity quickly.
Common Pitfalls
1. Incorrect Handling of Number 1
Pitfall: A common mistake is treating 1 like any other number in the DP update loop. Since 1 has no prime factors, it doesn't affect which primes are used in the product, but it does affect the count of valid subsets.
Wrong approach:
# Processing 1 in the main loop like other numbers
for number in range(1, 31): # Starting from 1 instead of 2
if number == 1:
# Incorrect: trying to update states with mask = 0
for state in range((1 << num_primes) - 1, 0, -1):
dp[state] = (dp[state] + frequency[1] * dp[state]) % MOD
Correct approach:
# Handle 1 separately before the main loop
dp[0] = pow(2, frequency[1], MOD) # Can choose any subset of 1's
# Then process numbers from 2 to 30
for number in range(2, 31):
# ... rest of the logic
2. Not Checking for Perfect Square Divisors Correctly
Pitfall: Only checking if a number is a perfect square itself, rather than checking if it's divisible by any perfect square.
Wrong approach:
# Only checking if the number itself is a perfect square
import math
if int(math.sqrt(number))**2 == number:
continue
Correct approach:
# Check divisibility by all relevant perfect squares if number % 4 == 0 or number % 9 == 0 or number % 25 == 0: continue
3. Incorrect DP State Update Order
Pitfall: Updating states in ascending order can cause using already-updated values in the same iteration, leading to incorrect counting.
Wrong approach:
# Iterating states from small to large
for state in range(1, 1 << num_primes):
if state & prime_mask == prime_mask:
dp[state] = (dp[state] + frequency[number] * dp[state ^ prime_mask]) % MOD
Correct approach:
# Iterate from largest state to smallest
for state in range((1 << num_primes) - 1, 0, -1):
if state & prime_mask == prime_mask:
dp[state] = (dp[state] + frequency[number] * dp[state ^ prime_mask]) % MOD
4. Forgetting to Subtract 1 for Empty Subset
Pitfall: The problem asks for non-empty subsets, but the DP counts all subsets including the empty one.
Wrong approach:
# Returning the sum directly
return sum(dp) % MOD
Correct approach:
# Subtract 1 to exclude the empty subset
total = sum(dp) % MOD
return total - 1
5. Incorrect Modulo Operation
Pitfall: Forgetting to apply modulo at each step or applying it incorrectly when subtracting.
Wrong approach:
# Not handling potential negative result after subtraction
return (sum(dp) - 1) % MOD # Can be negative if sum(dp) % MOD == 0
Correct approach:
# Ensure non-negative result
total = sum(dp) % MOD
return (total - 1 + MOD) % MOD # Adding MOD ensures non-negative
# Or simply:
return total - 1 # Since total >= 1 after modulo
Which algorithm should you use to find a node that is close to the root of the tree?
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!