2572. Count the Number of Square-Free Subsets
Problem Description
You are given a positive integer array nums
that is 0-indexed. The task is to calculate the number of non-empty subsets of this array such that the product of the elements in any of these subsets is a square-free integer. A square-free integer is one that is not divisible by any perfect square other than 1. For instance, 6 is square-free since its divisors 1, 2, 3, and 6 are not perfect squares (other than 1), but 12 is not square-free because it's divisible by 4, which is a perfect square.
The subset must be considered square-free as an entirety, meaning that the product of all its elements mustn't have a square factor larger than 1. Note that subsets must be non-empty and can be created by removing any number of elements, except all, from the nums
array. Different combinations of indices to delete lead to different subsets.
Due to the potentially large number of square-free subsets, the result should be returned modulo 10^9 + 7
.
To summarize:
- A non-empty subset is square-free if its product is a square-free integer.
- Calculate the number of such subsets.
- The result is a large number, hence return modulo
10^9 + 7
.
Intuition
The core of the solution leverages bitwise operations to effectively manage the complexity of finding square-free subsets. The idea hinges on the fact that each prime factor can only appear once in the square-free subset's product, otherwise it would not be square-free. We use bitmasks to represent the subset of prime factors that are contained in each element's prime factorisation.
The first part of the solution involves setting up prime numbers and a bitmask for each number x
in the nums
array. We only consider the primes up to 29 because the problem specifies that nums
only contains positive integers up to 30, which means none of them will have a prime factor greater than 29. We also discount any numbers that are square numbers themselves (e.g., 4, 9, 25) as they cannot be part of a square-free product.
We create a bitmask for each prime, where each bit position corresponds to a particular prime. If a number includes a prime in its factorization, the corresponding bit is set to 1.
Then for each number and its count in nums
, we update our subset counts (f
) using their bitmasks. The subset counts (f
) keep track of all possible combinations of prime factors up to this point; they represent how many ways we can get a certain combination of prime factors from the subsets analyzed so far.
Each element in nums
is accounted for by evaluating the bitwise 'AND' (&
) of its bitmask with each state (combination of prime factors represented as a bitmask) in f
. If the result is the same as the bitmask, it means the current subset's product can be formed by multiplying the products represented by the state and the new element.
After processing all elements in nums
, the sum of all counts in f
gives the total number of square-free subsets. We subtract 1 because the empty subset is included in our calculations, but the problem asks for non-empty subsets only.
Learn more about Math, Dynamic Programming and Bitmask patterns.
Solution Approach
The solution approach involves a few key algorithmic strategies and data structures:
-
Bitmasking: The main tool used here is bitmasking, which is a way to represent subsets of primes. Each bit in an integer represents whether a specific prime number is in the product of the subset. With this, it's easy to manipulate and combine prime factors using bitwise operations.
-
Dynamic Programming (DP): The problem is approached using a bottom-up dynamic programming technique. An array
f
is maintained, wheref[state]
represents the number of ways to form a square-free product whose prime factors are indicated by thestate
. Here,state
is an integer where each bit corresponds to whether a prime factor (from a pre-defined list of primes) is present in the subset. -
Modular Arithmetic: To handle potentially large numbers, modular arithmetic is implemented (with the modulus given as
10^9 + 7
) to ensure that all intermediate and final calculations fit within standard integer range. -
Prime Factorization: As part of the pre-processing, only numbers up to 30 are considered, and so, a fixed list of primes that are less than 30 is used. Then, a mask is created for each number based on its prime factors.
-
Enumeration of Subsets: The actual enumeration of subsets is done by iterating over all possible states of prime combinations, represented by
f
. If a number has prime factors corresponding to a givenstate
, it could be included in subsets that don’t have these primes already – hence the states are updated to reflect the new combinations.
Let's break down the key parts of the reference solution:
- Initialize the primes list for reference and a counter for occurrences of each element in
nums
. - Set up a DP array
f
with1 << n
elements, initialized to 0. Here,n
is the number of primes considered, and1 << n
represents 2 to the power of n, which is the number of possible combinations of the primes. The first elementf[0]
is initially set to2^cnt[1]
to handle the special case of 1s innums
. - Iterate over each possible element
x
(from 2 to 30, since 1 is already handled) and continue only ifx
is not a square number (x % 4, x % 9, and x % 25
) and has a non-zero count in the array. - For each such element
x
, calculate its bitmask (mask
), which determines which primes (out of the first 10) divide it. - Then update the DP array
f
. For every state (existing combination of prime factors), check whether adding the new elementx
represented bymask
would still keep the product square-free. This is done by using the bitwise AND operation. If the condition is true, updatef[state]
by adding the count ofx
times the count of combinations without the new primes. - Finally, the sum of all elements in
f
minus 1 (for the empty subset) is the total number of non-empty, square-free subsets. This value is returned modulo10^9 + 7
.
By using this approach, the problem of counting subsets is reduced from a potentially exponential brute force check to a manageable DP solution that runs in polynomial time relative to the size of the input and the number of considered primes.
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 with an example. Consider the array nums = [1, 2, 3, 4, 5]
. We wish to find the number of non-empty, square-free subsets.
First, we initialize our primes list for reference, which in the context of nums
is [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
because we're only considering numbers up to 30. We also initialize an array f
with a size of 1 << 10
(1024) because we have 10 primes and thus 2^10 possible combinations.
We handle the number 1 as a special case, setting f[0] = 2
because there is one number 1
in nums
, and it can be present or absent to form a subset (hence the power of 2). The 0
state here represents an empty prime factor set, which corresponds to any subset solely containing any number of 1's.
For each number x
in nums
that isn't a square number (4
from our example is excluded):
-
We calculate a bitmask representing the prime factors of
x
. For example, the number 2 has a bitmask of0000000001
, 3 has0000000010
, and 5 has0000000100
, etc. -
We iterate through the DP array
f
, checking eachstate
. To consider adding the numberx
to a subset represented bystate
, we perform(state & mask) == 0
. If this is true,x
can be added without making the subset's product divisible by a perfect square. We then updatef[new state]
wherenew state = state | mask
, meaning we add the prime factors ofx
to the state. This operation is carried out under modulo10^9 + 7
.
For the number 2, which has a bitmask of 0000000001
:
f[0]
represents subsets without any of the prime numbers considered, so we updatef[0000000001]
to bef[0000000001] + f[0]
.
For the number 3, which has a bitmask of 0000000010
:
f[0]
andf[0000000001]
are valid states that could add 3 without creating a square number, so we updatef[0000000010]
andf[0000000011]
accordingly.
For the number 5, with the bitmask 0000000100
:
- Again, we look for states where adding 5 does not introduce a square. This leads to updates of states like
f[0000000100]
,f[0000000101]
, andf[0000000110]
.
Following this approach for all permissible x
in nums
, we sum all the valid subsets from the DP array f
. The total count of square-free subsets would then be the sum of f
minus 1 (the initial state) to exclude the empty set.
Finally, we take the result modulo 10^9 + 7
. If we follow through with the process for all eligible numbers in the provided array nums
, we shall arrive at the required count of square-free subsets.
Solution Implementation
1from collections import Counter
2
3class Solution:
4 def square_free_subsets(self, nums: List[int]) -> int:
5 # Define the list of prime numbers up to 30
6 primes = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
7 # Count the occurrences of each number in the input list
8 count = Counter(nums)
9 # Define the modulo value for large numbers to avoid overflow
10 mod = 10 ** 9 + 7
11 # The length of the primes list
12 n = len(primes)
13 # Initialize the count of subsets without square factors (bitmask DP array)
14 subsets_count = [0] * (1 << n)
15 # Handle the special case for the number 1
16 subsets_count[0] = pow(2, count[1], mod)
17
18 # Loop through all the numbers up to 30
19 for x in range(2, 31):
20 # Skip numbers with zero count and perfect square factors
21 if count[x] == 0 or x % 4 == 0 or x % 9 == 0 or x % 25 == 0:
22 continue
23 # Initialize a bitmask to represent current number's prime factors
24 mask = 0
25 # Determine the bitmask for the prime factors of x
26 for i, prime in enumerate(primes):
27 if x % prime == 0:
28 mask |= 1 << i
29 # Update the subsets_count array by considering new combinations with x
30 for state in range((1 << n) - 1, 0, -1):
31 if state & mask == mask:
32 subsets_count[state] = (subsets_count[state] +
33 count[x] * subsets_count[state ^ mask]) % mod
34 # Calculate the total number of subsets by summing the counts
35 # Subtract one to exclude the empty set
36 return (sum(subsets_count) % mod) - 1
37
1class Solution {
2 public int squareFreeSubsets(int[] nums) {
3 // Prime numbers up to 29, will be used to check for square-free numbers.
4 int[] primes = {2, 3, 5, 7, 11, 13, 17, 19, 23, 29};
5
6 // Count array to keep track of occurrences of numbers between 1 and 30.
7 int[] count = new int[31];
8 for (int num : nums) {
9 ++count[num];
10 }
11
12 // Define modulus for large numbers to keep within integer limits.
13 final int mod = (int) 1e9 + 7;
14
15 // The number of prime numbers in the primes array.
16 int primeCount = primes.length;
17
18 // Dynamic programming array to store interim results.
19 // Each index represents a unique combination of primes (using bitmasks).
20 long[] dp = new long[1 << primeCount];
21
22 // There's always at least one subset: the empty set.
23 dp[0] = 1;
24
25 // Handle the special case for number 1.
26 for (int i = 0; i < count[1]; ++i) {
27 dp[0] = (dp[0] * 2) % mod; // Multiply by 2 for each occurrence of 1.
28 }
29
30 // Iterate over possible numbers (2 to 30) and calculate subsets.
31 for (int num = 2; num < 31; ++num) {
32 // Skip if there are no occurrences or if the number is a perfect square.
33 if (count[num] == 0 || num % 4 == 0 || num % 9 == 0 || num % 25 == 0) {
34 continue;
35 }
36
37 // Create a bitmask representing the prime factors of the current number.
38 int mask = 0;
39 for (int i = 0; i < primeCount; ++i) {
40 if (num % primes[i] == 0) {
41 mask |= 1 << i;
42 }
43 }
44
45 // Update the dynamic programming array
46 for (int state = (1 << primeCount) - 1; state > 0; --state) {
47 // If the current state includes all prime factors of num
48 if ((state & mask) == mask) {
49 // Add the new sets created by including 'num'
50 dp[state] = (dp[state] + count[num] * dp[state ^ mask]) % mod;
51 }
52 }
53 }
54
55 // Calculate the answer which is the sum of all sets formed.
56 long ans = 0;
57 for (long subsets : dp) {
58 ans = (ans + subsets) % mod;
59 }
60
61 // Subtract 1 to exclude the empty set from the final answer.
62 ans -= 1;
63
64 // Cast to int since the modulus guarantees the result is within int range.
65 return (int) ans;
66 }
67}
68
1class Solution {
2public:
3 int squareFreeSubsets(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 // Counter for occurrences of numbers (1 to 30)
7 int count[31]{};
8 // Populate the count array based on input nums
9 for (int& x : nums) {
10 ++count[x];
11 }
12 // Number of prime numbers we're considering
13 const int numOfPrimes = 10;
14 // Modulo constant for large numbers
15 const int mod = 1e9 + 7;
16
17 // f[i] will store the number of subsets ending with a subset of the prime state i
18 vector<long long> subsetsState(1 << numOfPrimes);
19 subsetsState[0] = 1;
20 // Handle the special case for the number 1
21 for (int i = 0; i < count[1]; ++i) {
22 subsetsState[0] = subsetsState[0] * 2 % mod;
23 }
24
25 // Iterate over all numbers from 2 to 30 to construct the subsetsState
26 for (int x = 2; x < 31; ++x) {
27 // Skip numbers that are squares or not present in the input array
28 if (count[x] == 0 || x % 4 == 0 || x % 9 == 0 || x % 25 == 0) {
29 continue;
30 }
31
32 // Determine the prime factors mask for the current number
33 int mask = 0;
34 for (int i = 0; i < numOfPrimes; ++i) {
35 if (x % primes[i] == 0) {
36 mask |= 1 << i;
37 }
38 }
39
40 // Update the subsetsState based on the current number's prime factors
41 for (int state = (1 << numOfPrimes) - 1; state; --state) {
42 if ((state & mask) == mask) {
43 subsetsState[state] = (subsetsState[state] + 1LL * count[x] * subsetsState[state ^ mask]) % mod;
44 }
45 }
46 }
47
48 // Calculate the final answer excluding the empty set
49 long long ans = -1;
50 for (auto state : subsetsState) {
51 ans = (ans + state) % mod;
52 }
53
54 // Return the total count of square-free subsets
55 return ans;
56 }
57};
58
1const PRIMES: number[] = [2, 3, 5, 7, 11, 13, 17, 19, 23, 29];
2const MOD: number = 1e9 + 7;
3const NUM_OF_PRIMES: number = 10;
4let count: number[] = new Array(31).fill(0);
5let subsetsState: number[] = new Array(1 << NUM_OF_PRIMES).fill(0);
6
7// Function to compute the total count of square-free subsets
8function squareFreeSubsets(nums: number[]): number {
9 // Populate the count array based on input nums
10 nums.forEach(x => {
11 count[x]++;
12 });
13
14 subsetsState[0] = 1;
15 // Handle the special case for the number 1 by doubling the subsets
16 for (let i: number = 0; i < count[1]; i++) {
17 subsetsState[0] = (subsetsState[0] * 2) % MOD;
18 }
19
20 // Iterate over all numbers from 2 to 30 to construct the subsetsState
21 for (let x = 2; x < 31; x++) {
22 // If the number is square or not in the input array, skip it
23 if (count[x] == 0 || x % 4 == 0 || x % 9 == 0 || x % 25 == 0) continue;
24
25 // Determine the prime factors mask for the current number x
26 let mask: number = 0;
27 for (let i = 0; i < NUM_OF_PRIMES; i++) {
28 if (x % PRIMES[i] == 0) {
29 mask |= 1 << i;
30 }
31 }
32
33 // Update subsetsState based on the prime factors of the current number
34 for (let state = (1 << NUM_OF_PRIMES) - 1; state > 0; state--) {
35 if ((state & mask) === mask) {
36 subsetsState[state] = (subsetsState[state] + count[x] * subsetsState[state ^ mask]) % MOD;
37 }
38 }
39 }
40
41 // Calculate the final answer excluding the empty set
42 let answer: number = -1;
43 subsetsState.forEach(state => {
44 answer = (answer + state) % MOD;
45 });
46
47 // Return the total count of square-free subsets
48 return answer;
49}
50
Time and Space Complexity
Time Complexity
The time complexity of the algorithm is primarily determined by the number of iterations in the nested loops.
The outer for-loop iterates over a range of numbers from 2 to 30, which is fixed and doesn't scale with input size. Therefore, its contribution to time complexity is constant, O(1)
.
However, the critical part is the nested loop iterating over the possible states represented by the binary mask. Since there are n
primes being considered, and each prime corresponds to a bit in the mask, there are 2^n
possible states. The loop iterates through all those states for each x
considered in the outer loop, which results in a total of O(2^n)
iterations for the inner loop.
Given that n
corresponds to the length of the primes list, which contains 10 primes, n = 10
. Thus, the dominating term for the time complexity is O(2^10)
, which simplifies to O(1024)
.
Because the outer for-loop's complexity is constant, and the inner loop runs in O(1024)
, the overall time complexity remains O(1024)
, which can be considered constant-time, O(1)
, because it does not vary with the input size.
Space Complexity
The space complexity can be analyzed by looking at the space allocated for data structures independent of the input size.
Here, the primary data structures are:
- The
primes
list, which contains a constant number of elements, contributingO(1)
space. - The
cnt
counter, which may contain up to 30 key-value pairs—one for each number from 2 to 30—yieldingO(1)
space. - The
f
list, which holds2^n
elements wheren
is the number of prime numbers considered (10 in this case). Thus, the space complexity for the listf
isO(1024)
.
As 1024
is a constant number, the space required for f
does not change with input size, and its contribution to space complexity is O(1)
.
Adding these together, the overall space complexity of the algorithm is O(1)
since all components contribute constant space.
In summary, the time complexity is O(1)
and the space complexity is O(1)
. It should be noted that while mathematically expressed as O(1)
, in practice, this might be considered inefficient due to the large constant factors involved.
Learn more about how to find time and space complexity quickly using problem constraints.
Which of the following array represent a max heap?
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!