Facebook Pixel

2638. Count the Number of K-Free Subsets 🔒

Problem Description

You are given an integer array nums containing distinct elements and an integer k.

A k-Free subset is defined as a subset that contains no two elements with an absolute difference equal to k. In other words, for any two elements a and b in a k-Free subset, |a - b| ≠ k.

Note that the empty set is considered a k-Free subset.

Your task is to find and return the total number of k-Free subsets that can be formed from the array nums.

A subset of an array is any selection of elements from the array, including the possibility of selecting no elements (empty set) or all elements.

For example, if nums = [1, 3, 5] and k = 2, then:

  • The subset [1, 5] is not k-Free because |5 - 1| = 4 ≠ 2
  • The subset [1, 3] is not k-Free because |3 - 1| = 2 = k
  • The subset [3, 5] is not k-Free because |5 - 3| = 2 = k
  • Valid k-Free subsets would be: [], [1], [3], [5], [1, 5]
Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

The key insight is that two numbers can have an absolute difference of k only if they have the same remainder when divided by k.

Think about it: if we have two numbers a and b where a = q1 * k + r and b = q2 * k + r (same remainder r), then |a - b| = |q1 * k - q2 * k| = |q1 - q2| * k, which is a multiple of k. This means the difference could potentially be exactly k (when |q1 - q2| = 1).

On the other hand, if two numbers have different remainders modulo k, their absolute difference can never be exactly k. For example, if a = q1 * k + r1 and b = q2 * k + r2 where r1 ≠ r2, then |a - b| = |q1 * k + r1 - q2 * k - r2| = |(q1 - q2) * k + (r1 - r2)|, which cannot equal k since |r1 - r2| < k and is non-zero.

This observation leads us to group numbers by their remainder modulo k. Elements from different groups can always be combined freely in any subset since they'll never violate the k-Free constraint. The challenge is within each group.

Within each group, after sorting, consecutive elements might have a difference of exactly k (like 3, 3+k, 3+2k, ...). This creates a dependency chain where choosing one element affects which others we can choose. This pattern is similar to the "House Robber" problem - we can't pick adjacent elements that differ by k, but we want to count all valid combinations.

For each group, we use dynamic programming where f[i] represents the number of valid subsets using the first i elements. When consecutive elements differ by k, we face a choice: either skip the current element (giving us f[i-1] ways) or include it but skip the previous one (giving us f[i-2] ways). When consecutive elements don't differ by k, we can freely include or exclude the current element, doubling our possibilities.

Since groups are independent, we multiply the number of valid subsets from each group to get the final answer.

Learn more about Math, Dynamic Programming, Combinatorics and Sorting patterns.

Solution Approach

The implementation follows these steps:

1. Sort and Group by Remainder

First, we sort the array nums in ascending order. Then we group elements by their remainder when divided by k. We use a dictionary g where the key is x % k and the value is a list of elements with that remainder.

nums.sort()
g = defaultdict(list)
for x in nums:
    g[x % k].append(x)

2. Dynamic Programming for Each Group

For each group arr in g.values(), we calculate the number of valid subsets using dynamic programming:

  • Initialize f[0] = 1 (empty subset)
  • Initialize f[1] = 2 (empty set and the set containing just the first element)

For each position i from 2 to m (where m is the length of the group):

  • Case 1: If arr[i-1] - arr[i-2] == k

    • The current and previous elements differ by exactly k, so we can't include both
    • If we include arr[i-1], we must exclude arr[i-2], giving us f[i-2] ways
    • If we exclude arr[i-1], we have all the ways from f[i-1]
    • Total: f[i] = f[i-1] + f[i-2]
  • Case 2: If arr[i-1] - arr[i-2] != k

    • The current and previous elements can both be included without restriction
    • For each subset counted in f[i-1], we can either include or exclude arr[i-1]
    • Total: f[i] = f[i-1] * 2
for i in range(2, m + 1):
    if arr[i - 1] - arr[i - 2] == k:
        f[i] = f[i - 1] + f[i - 2]
    else:
        f[i] = f[i - 1] * 2

3. Multiply Results from All Groups

Since elements from different groups can be combined freely (they never violate the k-Free constraint), we multiply the number of valid subsets from each group:

ans = 1
for arr in g.values():
    # ... calculate f[m] for this group ...
    ans *= f[m]
return ans

The time complexity is O(n log n) for sorting plus O(n) for the dynamic programming, giving O(n log n) overall. The space complexity is O(n) for storing the groups and DP array.

Ready to land your dream job?

Unlock your dream job with a 3-minute evaluator for a personalized learning plan!

Start Evaluator

Example Walkthrough

Let's work through an example with nums = [2, 3, 5, 8] and k = 3.

Step 1: Sort and Group by Remainder (mod k)

After sorting (already sorted), we group by remainder when divided by 3:

  • 2 % 3 = 2 → group 2: [2, 5, 8]
  • 3 % 3 = 0 → group 0: [3]

Step 2: Process Group 0 ([3])

With only one element, we have 2 subsets: [] and [3]

  • f[0] = 1 (empty subset)
  • f[1] = 2 (empty + [3])
  • Result for this group: 2

Step 3: Process Group 2 ([2, 5, 8])

Initialize:

  • f[0] = 1 (empty subset)
  • f[1] = 2 (empty + [2])

For position 2 (element 5):

  • Check: 5 - 2 = 3 = k
  • Elements 2 and 5 differ by exactly k, so we can't have both
  • f[2] = f[1] + f[0] = 2 + 1 = 3
  • Valid subsets so far: [], [2], [5]

For position 3 (element 8):

  • Check: 8 - 5 = 3 = k
  • Elements 5 and 8 differ by exactly k, so we can't have both
  • f[3] = f[2] + f[1] = 3 + 2 = 5
  • Valid subsets: [], [2], [5], [8], [2, 8]

Result for this group: 5

Step 4: Multiply Results

Total k-Free subsets = 2 × 5 = 10

These 10 subsets are:

  1. [] (empty)
  2. [2]
  3. [3]
  4. [5]
  5. [8]
  6. [2, 3] (different groups, OK)
  7. [3, 5] (different groups, OK)
  8. [3, 8] (different groups, OK)
  9. [2, 8] (same group but |8-2| = 6 ≠ 3, OK)
  10. [2, 3, 8] (combining valid selections from each group)

Note that subsets like [2, 5], [5, 8], [3, 5, 8], etc. are invalid because they contain pairs with difference exactly k = 3.

Solution Implementation

1from typing import List
2from collections import defaultdict
3
4class Solution:
5    def countTheNumOfKFreeSubsets(self, nums: List[int], k: int) -> int:
6        # Sort the input array for processing consecutive elements
7        nums.sort()
8      
9        # Group numbers by their remainder when divided by k
10        # Elements with same remainder mod k can potentially differ by k
11        groups = defaultdict(list)
12        for num in nums:
13            groups[num % k].append(num)
14      
15        # Initialize result as 1 (will multiply counts from each group)
16        result = 1
17      
18        # Process each group independently
19        for group_values in groups.values():
20            group_size = len(group_values)
21          
22            # dp[i] represents number of valid subsets using first i elements
23            dp = [0] * (group_size + 1)
24            dp[0] = 1  # Empty subset
25            dp[1] = 2  # Empty subset and subset with first element
26          
27            # Build up the dp array
28            for i in range(2, group_size + 1):
29                # Check if current and previous elements differ by exactly k
30                if group_values[i - 1] - group_values[i - 2] == k:
31                    # Elements are adjacent with difference k - can't both be selected
32                    # Either include current (exclude previous) or exclude current
33                    dp[i] = dp[i - 1] + dp[i - 2]
34                else:
35                    # Elements are independent - current element doubles possibilities
36                    dp[i] = dp[i - 1] * 2
37          
38            # Multiply the count for this group to the total result
39            result *= dp[group_size]
40      
41        return result
42
1class Solution {
2    public long countTheNumOfKFreeSubsets(int[] nums, int k) {
3        // Sort the array to process elements in ascending order
4        Arrays.sort(nums);
5      
6        // Group numbers by their remainder when divided by k
7        // Numbers with the same remainder modulo k need special handling
8        Map<Integer, List<Integer>> groupsByModulo = new HashMap<>();
9        for (int i = 0; i < nums.length; ++i) {
10            int remainder = nums[i] % k;
11            groupsByModulo.computeIfAbsent(remainder, key -> new ArrayList<>()).add(nums[i]);
12        }
13      
14        // Calculate the total number of k-free subsets
15        long totalCount = 1;
16      
17        // Process each group independently
18        for (List<Integer> group : groupsByModulo.values()) {
19            int groupSize = group.size();
20          
21            // dp[i] represents the number of k-free subsets using first i elements
22            long[] dp = new long[groupSize + 1];
23            dp[0] = 1;  // Empty subset
24            dp[1] = 2;  // Empty subset and subset with first element
25          
26            // Build up the dp array
27            for (int i = 2; i <= groupSize; ++i) {
28                // Check if current and previous elements differ by exactly k
29                if (group.get(i - 1) - group.get(i - 2) == k) {
30                    // If they differ by k, we cannot include both
31                    // Either include subsets ending at (i-1) or subsets ending at (i-2) plus current
32                    dp[i] = dp[i - 1] + dp[i - 2];
33                } else {
34                    // If they don't differ by k, current element is independent
35                    // We can either include or exclude it from any previous subset
36                    dp[i] = dp[i - 1] * 2;
37                }
38            }
39          
40            // Multiply the count for this group with the running total
41            totalCount *= dp[groupSize];
42        }
43      
44        return totalCount;
45    }
46}
47
1class Solution {
2public:
3    long long countTheNumOfKFreeSubsets(vector<int>& nums, int k) {
4        // Sort the array to process elements in ascending order
5        sort(nums.begin(), nums.end());
6      
7        // Group numbers by their remainder when divided by k
8        // Numbers with the same remainder modulo k can potentially violate the k-free condition
9        unordered_map<int, vector<int>> groupsByModulo;
10        for (int i = 0; i < nums.size(); ++i) {
11            groupsByModulo[nums[i] % k].push_back(nums[i]);
12        }
13      
14        long long totalCount = 1;
15      
16        // Process each group independently
17        for (auto& [remainder, group] : groupsByModulo) {
18            int groupSize = group.size();
19          
20            // dp[i] represents the number of k-free subsets using the first i elements of the group
21            long long dp[groupSize + 1];
22          
23            // Base cases
24            dp[0] = 1;  // Empty subset
25            dp[1] = 2;  // Empty subset or subset with first element
26          
27            // Fill the DP table
28            for (int i = 2; i <= groupSize; ++i) {
29                // Check if current element and previous element differ by exactly k
30                if (group[i - 1] - group[i - 2] == k) {
31                    // If they differ by k, we cannot include both
32                    // dp[i] = subsets excluding current element + subsets excluding previous element
33                    dp[i] = dp[i - 1] + dp[i - 2];
34                } else {
35                    // If they don't differ by k, current element is independent
36                    // We can either include or exclude it, doubling the possibilities
37                    dp[i] = dp[i - 1] * 2;
38                }
39            }
40          
41            // Multiply the count for this group with the total count
42            totalCount *= dp[groupSize];
43        }
44      
45        return totalCount;
46    }
47};
48
1/**
2 * Counts the number of k-free subsets in the given array.
3 * A k-free subset is a subset where no two elements have a difference of k.
4 * 
5 * @param nums - The input array of numbers
6 * @param k - The difference value to avoid in subsets
7 * @returns The count of k-free subsets
8 */
9function countTheNumOfKFreeSubsets(nums: number[], k: number): number {
10    // Sort the array in ascending order
11    nums.sort((a, b) => a - b);
12  
13    // Group numbers by their remainder when divided by k
14    // This helps identify numbers that could potentially have a difference of k
15    const groupsByModulo: Map<number, number[]> = new Map();
16  
17    for (const num of nums) {
18        const remainder = num % k;
19      
20        // Initialize the group if it doesn't exist
21        if (!groupsByModulo.has(remainder)) {
22            groupsByModulo.set(remainder, []);
23        }
24      
25        // Add the number to its corresponding group
26        groupsByModulo.get(remainder)!.push(num);
27    }
28  
29    // Initialize the result as 1 (multiplicative identity)
30    let result: number = 1;
31  
32    // Process each group independently
33    for (const [_, group] of groupsByModulo) {
34        const groupSize = group.length;
35      
36        // Dynamic programming array to store the number of valid subsets
37        // dp[i] represents the number of valid subsets using the first i elements
38        const dp: number[] = new Array(groupSize + 1).fill(1);
39      
40        // Base case: with 1 element, we can have 2 subsets (empty set and the element itself)
41        dp[1] = 2;
42      
43        // Fill the dp array for positions 2 to groupSize
44        for (let i = 2; i <= groupSize; ++i) {
45            // Check if current and previous elements have a difference of k
46            if (group[i - 1] - group[i - 2] === k) {
47                // If difference is k, we can't include both elements
48                // So we either take subsets up to i-1 or add subsets up to i-2
49                dp[i] = dp[i - 1] + dp[i - 2];
50            } else {
51                // If difference is not k, we can freely include or exclude current element
52                // This doubles the number of possible subsets
53                dp[i] = dp[i - 1] * 2;
54            }
55        }
56      
57        // Multiply the result by the number of valid subsets for this group
58        result *= dp[groupSize];
59    }
60  
61    return result;
62}
63

Time and Space Complexity

Time Complexity: O(n × log n)

The time complexity is dominated by the following operations:

  • Sorting the array: O(n × log n) where n is the length of nums
  • Grouping elements by modulo k: O(n) - single pass through the array
  • Processing each group: For each group, we iterate through its elements once to compute the dynamic programming array f. Since all elements are distributed among the groups and each element is processed once, this takes O(n) total time across all groups
  • The overall time complexity is O(n × log n) + O(n) = O(n × log n)

Space Complexity: O(n)

The space complexity consists of:

  • The dictionary g storing all elements grouped by their modulo k value: O(n) in the worst case when all elements are stored
  • The dynamic programming array f for each group: The maximum size is m + 1 where m is the size of the largest group. In the worst case, all elements could be in one group, making this O(n)
  • The total space complexity is O(n)

Learn more about how to find time and space complexity quickly.

Common Pitfalls

Pitfall 1: Incorrect Handling of Elements with Different Remainders

The Problem: A common misconception is assuming that elements with different remainders modulo k can never violate the k-free constraint. While the solution correctly groups by remainder, developers might incorrectly think this is just an optimization rather than a critical correctness requirement.

Why It Happens: Consider nums = [1, 4, 7] and k = 3:

  • All elements have different remainders: 1%3=1, 4%3=1, 7%3=1 (wait, they're the same!)
  • But even with different remainders like nums = [1, 3, 8] and k = 5, we have 1%5=1, 3%5=3, 8%5=3
  • Elements 3 and 8 both have remainder 3, and |8-3|=5=k, so they can't coexist

The Fix: The solution correctly handles this by grouping elements with the same remainder together and processing each group independently. Within a group, consecutive sorted elements might differ by exactly k.

Pitfall 2: Off-by-One Errors in DP Array Indexing

The Problem: The DP array uses 1-based indexing conceptually (dp[i] represents subsets using first i elements) but the group array uses 0-based indexing. This can lead to confusion when accessing group_values[i-1] and group_values[i-2].

Example of the Error:

# Incorrect version - mixing up indices
for i in range(1, group_size):  # Wrong range!
    if group_values[i] - group_values[i - 1] == k:  # Wrong indices!
        dp[i] = dp[i - 1] + dp[i - 2]  # This would fail

The Fix: Always remember:

  • dp[i] represents the count for the first i elements
  • When i = 2, we're looking at elements at indices 0 and 1 in the group
  • So we check group_values[i-1] - group_values[i-2] which is group_values[1] - group_values[0]

Pitfall 3: Forgetting the Base Cases

The Problem: Not properly initializing dp[0] = 1 and dp[1] = 2 or misunderstanding what they represent.

Why It Matters:

  • dp[0] = 1 represents the empty subset (there's exactly one way to select nothing)
  • dp[1] = 2 represents two possibilities: empty subset or the subset containing just the first element
  • Without these correct base cases, the entire DP calculation fails

Example Scenario: If you initialize dp[1] = 1 thinking it only represents the single element subset, you miss counting the empty subset, leading to incorrect results for the entire group.

Pitfall 4: Integer Overflow in Large Test Cases

The Problem: When multiplying results from multiple groups or when a group has many independent elements (the dp[i] = dp[i-1] * 2 case), the numbers can grow exponentially large.

Example: If you have 30 independent elements in a group, dp[30] would be 2^30 = 1,073,741,824. With multiple such groups, the result could exceed typical integer limits in some languages.

The Fix: In Python, this isn't an issue due to arbitrary precision integers. In other languages, you might need to:

  • Use long/int64 data types
  • Apply modulo arithmetic if the problem specifies it
  • Check for overflow conditions

Correct Implementation Check: Always verify your solution with edge cases like:

  • nums = [1], k = 1 → should return 2 (empty set and {1})
  • nums = [1, 2, 3], k = 1 → should return 1 (only empty set is valid)
  • Large arrays with many independent elements to check for overflow
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

Is the following code DFS or BFS?

void search(Node root) {
  if (!root) return;
  visit(root);
  root.visited = true;
  for (Node node in root.adjacent) {
    if (!node.visited) {
      search(node);
    }
  }
}

Recommended Readings

Want a Structured Path to Master System Design Too? Don’t Miss This!

Load More