Count the Number of Square-Free Subsets
MedPrompt
You are given a positive integer 0-indexed array nums.
A subset of the array nums is square-free if the product of its elements is a square-free integer.
A square-free integer is an integer that is divisible by no square number other than 1.
Return the number of square-free non-empty subsets of the array nums. Since the answer may be too large, return it modulo 109 + 7.
A non-empty subset of nums is an array that can be obtained by deleting some (possibly none but not all) elements from nums. Two subsets are different if and only if the chosen indices to delete are different.
Example 1:
Input: nums = [3,4,4,5]
Output: 3
Explanation: There are 3 square-free subsets in this example:
- The subset consisting of the 0th element [3]. The product of its elements is 3, which is a square-free integer.
- The subset consisting of the 3rd element [5]. The product of its elements is 5, which is a square-free integer.
- The subset consisting of 0th and 3rd elements [3,5]. The product of its elements is 15, which is a square-free integer.
It can be proven that there are no more than 3 square-free subsets in the given array.Example 2:
Input: nums = [1]
Output: 1
Explanation: There is 1 square-free subset in this example:
- The subset consisting of the 0th element [1]. The product of its elements is 1, which is a square-free integer.
It can be proven that there is no more than 1 square-free subset in the given array.
Constraints:
1 <= nums.length <= 10001 <= nums[i] <= 30
Approaches
3 approaches with complexity analysis and trade-offs.
This approach uses a recursive backtracking algorithm. We first process the input array to count the frequencies of each number and filter out non-square-free numbers. The core of the approach is a recursive function that explores the choices of including or not including each unique square-free number to form a valid subset. A subset is valid if the prime factors of the newly added number do not overlap with the prime factors of the numbers already in the subset. We use a bitmask to efficiently track the set of prime factors used.
Algorithm
- Pre-process the input
numsto count the frequency of each number. - Identify the unique square-free numbers greater than 1 present in
nums. - Implement a recursive function, say
solve(index, mask), whereindexrefers to the current unique square-free number being considered, andmaskis a bitmask representing the prime factors used by the subset constructed so far. - In the recursive function, for each number, explore two possibilities:
a. Exclude the number: Make a recursive call
solve(index + 1, mask). b. Include the number: If its prime factor mask does not conflict with the currentmask, make a recursive callsolve(index + 1, new_mask)and multiply the result by the frequency of the number. This accounts for the different ways to choose this number if it appears multiple times. - The base case for the recursion is when all unique numbers have been considered (
indexreaches the end), at which point we return 1 (representing one validly formed subset). - Sum the results from the recursive calls to get the total count of square-free subsets formed from numbers greater than 1 (let's call this
C'). - The number 1 can be part of any square-free subset. If there are
kones innums, they can form2^ksubsets (including the empty one). The total number of square-free subsets isC' * 2^k. - Subtract 1 from the total to exclude the overall empty subset and return the result modulo
10^9 + 7.
Walkthrough
First, we can simplify the problem. Any number in nums that is not square-free (i.e., divisible by 4, 9, 25, etc.) can never be part of a square-free subset, so we can ignore them. The number 1 is square-free and coprime with any other number, so it can be combined with any square-free subset. We can handle all occurrences of 1s at the end.
The problem then reduces to finding the number of square-free subsets of nums containing only square-free numbers greater than 1. A subset is square-free if and only if all its elements are square-free and pairwise coprime. This means their sets of prime factors must be disjoint.
We can represent the set of prime factors of a number using a bitmask. Since all numbers are at most 30, we only need to consider primes up to 30, which are {2, 3, 5, 7, 11, 13, 17, 19, 23, 29} (10 primes).
The algorithm proceeds as follows:
- Count the frequency of each number in
nums. - Let
count1be the frequency of the number 1. - Create a list of unique square-free numbers > 1 present in
nums. - A recursive function
solve(idx, mask)is defined, which calculates the number of valid subsets using numbers fromunique_sf_nums[idx:], given that the primes represented bymaskare already used. - In
solve(idx, mask), we have two choices forunique_sf_nums[idx]:- Don't include it: Recursively call
solve(idx + 1, mask). - Include it: This is possible only if its prime factor mask doesn't overlap with
mask. If so, we addcount(num) * solve(idx + 1, new_mask)to the result, wherecount(num)is the frequency of the number.
- Don't include it: Recursively call
- The base case for the recursion is when
idxreaches the end of the list, in which case we return 1 (for the one valid subset formed). - The initial call is
solve(0, 0). Let the result beC'. This is the number of square-free subsets (including empty) from numbers > 1. - The total number of subsets is
C' * 2^count1. The final answer is this value minus 1 (to exclude the all-empty subset).
class Solution { int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; int[] masks; long MOD = 1_000_000_007; List<Integer> uniqueSfNums; int[] counts; public int squareFreeSubsets(int[] nums) { counts = new int[31]; for (int num : nums) { counts[num]++; } masks = new int[31]; for (int i = 2; i <= 30; i++) { masks[i] = getMask(i); } uniqueSfNums = new ArrayList<>(); for (int i = 2; i <= 30; i++) { if (counts[i] > 0 && masks[i] != -1) { uniqueSfNums.add(i); } } long countGt1 = solve(0, 0); long count1 = counts[1]; long total = (countGt1 * power(2, count1)) % MOD; return (int)((total - 1 + MOD) % MOD); } private long solve(int idx, int mask) { if (idx == uniqueSfNums.size()) { return 1; } // Option 1: Don't include uniqueSfNums[idx] long res = solve(idx + 1, mask); // Option 2: Include uniqueSfNums[idx] int num = uniqueSfNums.get(idx); int numMask = masks[num]; if ((mask & numMask) == 0) { long ways = (counts[num] * solve(idx + 1, mask | numMask)) % MOD; res = (res + ways) % MOD; } return res; } private int getMask(int n) { int mask = 0; for (int i = 0; i < primes.length; i++) { int p = primes[i]; if (n % (p * p) == 0) return -1; // Not square-free if (n % p == 0) { mask |= (1 << i); } } return mask; } private long power(long base, long exp) { long res = 1; base %= MOD; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % MOD; base = (base * base) % MOD; exp /= 2; } return res; }}Complexity
Time
O(N + 2^U), where `N` is the length of `nums` and `U` is the number of unique square-free numbers present in `nums` (`U <= 18`). The `O(N)` is for preprocessing `nums`. The `2^U` comes from the binary recursion tree. Since `U` is small, this approach is feasible and can pass.
Space
O(U), where `U` is the number of unique square-free numbers in `nums`. This space is used by the recursion stack. Since `U` is at most 18, this is effectively constant space.
Trade-offs
Pros
Conceptually straightforward recursive solution that directly models the decision process.
Cons
Highly inefficient due to re-computation of results for the same state (index and mask).
The time complexity is exponential in the number of unique square-free elements, which might be too slow if the number of such elements were larger.
This approach optimizes the backtracking solution by using memoization to store and reuse the results of subproblems. This avoids redundant computations and significantly improves performance. The state for memoization is defined by the current index of the unique square-free number being considered and the current bitmask of used prime factors.
Algorithm
- The overall logic is identical to the plain backtracking approach.
- A memoization table (e.g., a 2D array
memo[index][mask]) is introduced to store the results of the recursive functionsolve(index, mask). - Before computing
solve(index, mask), check ifmemo[index][mask]already contains a result. If so, return the stored value. - If the result is not in the memoization table, compute it using the same recursive logic as before.
- After computing the result, store it in
memo[index][mask]before returning it. - The rest of the steps, including handling the number 1 and calculating the final answer, remain unchanged.
Walkthrough
The core idea is the same as the plain backtracking approach. We identify unique square-free numbers and their counts. We use a recursive function to build subsets, ensuring they are pairwise coprime using prime factor bitmasks.
The key difference is the addition of a memoization table, memo[idx][mask], which stores the result of solve(idx, mask).
Before computing solve(idx, mask), we first check if the result is already in our memoization table. If it is, we return the stored value immediately. Otherwise, we compute the result as in the backtracking approach and store it in the table before returning.
This technique transforms the exponential complexity of the plain recursion into a solution with complexity proportional to the number of states, which is much more efficient.
The rest of the logic, including handling the number 1 and calculating the final result, remains the same.
class Solution { int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; int[] masks; long MOD = 1_000_000_007; List<Integer> uniqueSfNums; int[] counts; Long[][] memo; public int squareFreeSubsets(int[] nums) { counts = new int[31]; for (int num : nums) { counts[num]++; } masks = new int[31]; for (int i = 2; i <= 30; i++) { masks[i] = getMask(i); } uniqueSfNums = new ArrayList<>(); for (int i = 2; i <= 30; i++) { if (counts[i] > 0 && masks[i] != -1) { uniqueSfNums.add(i); } } memo = new Long[uniqueSfNums.size()][1 << 10]; long countGt1 = solve(0, 0); long count1 = counts[1]; long total = (countGt1 * power(2, count1)) % MOD; return (int)((total - 1 + MOD) % MOD); } private long solve(int idx, int mask) { if (idx == uniqueSfNums.size()) { return 1; } if (memo[idx][mask] != null) { return memo[idx][mask]; } // Option 1: Don't include uniqueSfNums[idx] long res = solve(idx + 1, mask); // Option 2: Include uniqueSfNums[idx] int num = uniqueSfNums.get(idx); int numMask = masks[num]; if ((mask & numMask) == 0) { long ways = (counts[num] * solve(idx + 1, mask | numMask)) % MOD; res = (res + ways) % MOD; } return memo[idx][mask] = res; } private int getMask(int n) { int mask = 0; for (int i = 0; i < primes.length; i++) { int p = primes[i]; if (n % (p * p) == 0) return -1; if (n % p == 0) { mask |= (1 << i); } } return mask; } private long power(long base, long exp) { long res = 1; base %= MOD; while (exp > 0) { if (exp % 2 == 1) res = (res * base) % MOD; base = (base * base) % MOD; exp /= 2; } return res; }}Complexity
Time
O(N + U * 2^P), where `N` is `nums.length`, `U <= 18`, and `P = 10`. The complexity is dominated by filling the memoization table. Since `U` and `P` are small constants, this is effectively O(N).
Space
O(U * 2^P) for the memoization table, where `U` is the number of unique square-free numbers (`<=18`) and `P` is the number of primes (`=10`). This is effectively constant space.
Trade-offs
Pros
Much more efficient than plain backtracking by avoiding re-computation.
Guarantees that each subproblem is solved only once, leading to a polynomial time complexity in terms of states.
Cons
Requires extra space for the memoization table.
Has a slightly higher overhead than the iterative bottom-up approach due to recursion function calls.
This is the most optimized approach, using an iterative, bottom-up dynamic programming method. It eliminates recursion overhead entirely. We use a DP array, dp[mask], to store the number of square-free subsets whose combined prime factors are represented by mask. We iterate through each unique square-free number and update the DP table based on the subsets that can be formed by including this number.
Algorithm
- Pre-process
numsto get the frequency count of each number. Store the count of 1s separately. - Initialize a DP array,
dp, of size1024(for2^10prime masks).dp[mask]will store the number of square-free subsets with a combined prime factor mask equal tomask. - Set
dp[0] = 1, representing the single empty subset, which has a mask of 0. - Iterate through each number
numfrom 2 to 30. - If
numis present innumsand is square-free, get its prime factor mask,num_mask. - Iterate through all existing masks
jin thedptable (from1023down to0). The downward iteration is crucial to ensure that we use thedpvalues from before considering the current numbernum. - If
numcan be added to subsets with prime maskj(i.e.,(j & num_mask) == 0), update thedpentry for the new combined mask:dp[j | num_mask] = (dp[j | num_mask] + dp[j] * count(num)) % MOD. - After iterating through all numbers, sum all values in the
dparray to get the total number of square-free subsets (including the empty one) from numbers > 1. Let this beC'. - Calculate the final result as
(C' * 2^count(1) - 1) % MOD.
Walkthrough
This approach builds the solution iteratively. The state dp[mask] represents the number of ways to form a square-free subset (from numbers > 1 considered so far) with a combined prime factor mask of mask.
The algorithm is as follows:
- Pre-process
numsto get the frequency count of each number. Handle the count of 1s separately. - Initialize a DP array
dpof size1024(for2^10prime masks). Setdp[0] = 1(representing the empty subset) and all other entries to 0. - Iterate through each number
numfrom 2 to 30. - If
numis present innums(i.e.,count(num) > 0) and is square-free: a. Get its prime factor mask,num_mask. b. Iterate through all existing masksmaskin thedptable (from1023down to0to avoid usingnummultiple times within the same main step). c. Ifnumcan be added to subsets with prime maskmask(i.e.,(mask & num_mask) == 0), update thedpentry for the new combined mask:dp[mask | num_mask] += dp[mask] * count(num). - After iterating through all numbers, the sum of all values in the
dparray gives the total number of square-free subsets (including the empty one) that can be formed from numbers > 1. Let this beC'. - The final result is calculated as
(C' * 2^count(1) - 1) % MOD.
class Solution { public int squareFreeSubsets(int[] nums) { int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; long MOD = 1_000_000_007; int[] counts = new int[31]; for (int num : nums) { counts[num]++; } long[] dp = new long[1 << 10]; dp[0] = 1; for (int i = 2; i <= 30; i++) { if (counts[i] == 0) continue; int mask = 0; int temp = i; boolean isSquareFree = true; for (int j = 0; j < primes.length; j++) { int p = primes[j]; if (temp % (p * p) == 0) { isSquareFree = false; break; } if (temp % p == 0) { mask |= (1 << j); } } if (!isSquareFree) continue; for (int j = (1 << 10) - 1; j >= 0; j--) { if ((j & mask) == 0) { dp[j | mask] = (dp[j | mask] + dp[j] * counts[i]) % MOD; } } } long totalSubsets = 0; for (long count : dp) { totalSubsets = (totalSubsets + count) % MOD; } long count1 = counts[1]; long powerOf2 = 1; // Modular exponentiation is better, but this is fine for N<=1000 for (int i = 0; i < count1; i++) { powerOf2 = (powerOf2 * 2) % MOD; } totalSubsets = (totalSubsets * powerOf2) % MOD; return (int)((totalSubsets - 1 + MOD) % MOD); }}Complexity
Time
O(N + K * 2^P), where `N` is `nums.length`, `K` is the range of numbers (30), and `P` is the number of primes (10). Since `K` and `P` are constants, the complexity is linear in the size of the input, O(N).
Space
O(K + 2^P), where `K` is the range of numbers (31) and `P` is the number of primes (10). This is for the counts and DP arrays, which is constant space.
Trade-offs
Pros
Most efficient in practice due to being iterative and avoiding recursion overhead.
Clean and concise implementation.
Space efficient as it only requires a single DP array.
Cons
The logic might be slightly less intuitive to formulate compared to the recursive top-down approach for those more accustomed to recursion.
Solutions
Solution
class Solution {public int squareFreeSubsets(int[] nums) { int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; int[] cnt = new int[31]; for (int x : nums) { ++cnt[x]; } final int mod = (int)1 e9 + 7; int n = primes.length; long[] f = new long[1 << n]; f[0] = 1; for (int i = 0; i < cnt[1]; ++i) { f[0] = (f[0] * 2) % mod; } for (int x = 2; x < 31; ++x) { if (cnt[x] == 0 || x % 4 == 0 || x % 9 == 0 || x % 25 == 0) { continue; } int mask = 0; for (int i = 0; i < n; ++i) { if (x % primes[i] == 0) { mask |= 1 << i; } } for (int state = (1 << n) - 1; state > 0; --state) { if ((state & mask) == mask) { f[state] = (f[state] + cnt[x] * f[state ^ mask]) % mod; } } } long ans = 0; for (int i = 0; i < 1 << n; ++i) { ans = (ans + f[i]) % mod; } ans -= 1; return (int)ans; }}class Solution {public: int squareFreeSubsets(vector<int> &nums) { int primes[10] = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29}; int cnt[31]{}; for (int &x : nums) { ++cnt[x]; } int n = 10; const int mod = 1e9 + 7; vector<long long> f(1 << n); f[0] = 1; for (int i = 0; i < cnt[1]; ++i) { f[0] = f[0] * 2 % mod; } for (int x = 2; x < 31; ++x) { if (cnt[x] == 0 || x % 4 == 0 || x % 9 == 0 || x % 25 == 0) { continue; } int mask = 0; for (int i = 0; i < n; ++i) { if (x % primes[i] == 0) { mask |= 1 << i; } } for (int state = (1 << n) - 1; state; --state) { if ((state & mask) == mask) { f[state] = (f[state] + 1LL * cnt[x] * f[state ^ mask]) % mod; } } } long long ans = -1; for (int i = 0; i < 1 << n; ++i) { ans = (ans + f[i]) % mod; } return ans; }};class Solution: def squareFreeSubsets(self, nums: List[int]) -> int: primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] cnt = Counter(nums) mod = 10 ** 9 + 7 n = len(primes) f = [0] * (1 << n) f[0] = pow(2, cnt[1]) for x in range(2, 31): if cnt[x] == 0 or x % 4 == 0 or x % 9 == 0 or x % 25 == 0: continue 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 return sum(v for v in f) % mod - 1Video walkthrough
Newsletter
One sharp idea, every week
System design and interview prep — short enough to finish.
No spam. Unsubscribe anytime.
Practice
Same difficulty — related problems to reinforce the pattern.