Facebook Pixel

2597. The Number of Beautiful Subsets

Problem Description

You have an array nums containing positive integers and a positive integer k.

A subset of nums is considered beautiful if no two numbers in the subset have an absolute difference equal to k. In other words, for any two elements a and b in a beautiful subset, |a - b| ≠ k.

Your task is to count how many non-empty beautiful subsets can be formed from the array nums.

A subset is any collection of elements that can be created by removing some (or none) of the elements from nums. Two subsets are considered different if they are formed by removing different indices from the original array.

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

  • The subset [2, 5] is NOT beautiful because |5 - 2| = 3 = k
  • The subset [2, 9] is beautiful because |9 - 2| = 7 ≠ k
  • The subset [5, 9] is beautiful because |9 - 5| = 4 ≠ k

The solution uses backtracking with a counter to track selected numbers. For each number, it checks if adding it would violate the beautiful subset condition (by checking if nums[i] + k or nums[i] - k already exists in the current subset). The initial answer is set to -1 to exclude the empty subset from the final count.

Flowchart Walkthrough

First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:

Is it a graph?

  • No: The problem involves an array of integers and subset generation, not nodes and edges as in graph problems.

Need to solve for kth smallest/largest?

  • No: We're counting all valid subsets, not finding a specific kth element.

Involves Linked Lists?

  • No: The problem works with an array of integers, not linked list structures.

Does the problem have small constraints?

  • Yes: Subset problems inherently have exponential complexity (2^n possible subsets). The problem asks us to enumerate all non-empty beautiful subsets, which suggests the input size is manageable enough to explore all possibilities.

Brute force / Backtracking?

  • Yes: We need to explore all possible subsets and check if each one satisfies the "beautiful" condition (no two elements with absolute difference equal to k). Backtracking is perfect for this as we can:
    • Try including each element in our current subset
    • Check if it violates the beautiful condition
    • If valid, continue building the subset
    • Backtrack by removing the element and trying other possibilities

Conclusion: The flowchart correctly leads us to use a Backtracking approach. This makes sense because we need to generate and validate all possible subsets while maintaining a constraint (no two elements can differ by exactly k), which is a classic backtracking pattern where we build solutions incrementally and abandon paths that violate our constraints.

Quick Interview Experience
Help others by sharing your interview experience
Have you seen this problem before?

Intuition

When we need to count all valid subsets with a specific constraint, we're essentially exploring a decision tree where at each element, we decide whether to include it or not. This is the essence of backtracking.

For each element in the array, we face a binary choice: include it in our current subset or skip it. However, there's a catch - if we include an element x, we must ensure that neither x + k nor x - k already exists in our current subset.

Think of it like placing items on a shelf with a spacing rule: no two items can be exactly k units apart. As we build our subset, we need to track what we've already chosen to ensure we don't violate this spacing rule.

The key insight is that we can use a counter to track the elements currently in our subset. Before adding a new element nums[i], we check if nums[i] + k or nums[i] - k has a count greater than 0 in our counter. If either does, we can't add nums[i] without breaking the "beautiful" property.

The backtracking pattern emerges naturally:

  1. At each position, first try skipping the current element (this always works)
  2. Then check if we can include the current element by verifying the constraint
  3. If we can include it, temporarily add it to our counter, explore further, then remove it (backtrack)

We start with ans = -1 because the recursive function will count the empty subset as one valid subset when it reaches the end of the array, but the problem asks for non-empty subsets only. By initializing to -1, we effectively subtract the empty subset from our final count.

This approach systematically explores all 2^n possible subsets while pruning invalid branches early, making it efficient for the small constraints typical of subset problems.

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

Solution Approach

The solution implements a depth-first search (DFS) with backtracking to explore all possible subsets while maintaining the beautiful subset constraint.

Data Structure:

  • We use a Counter (hash table) to track the frequency of elements currently in our subset. This allows O(1) lookups to check if nums[i] + k or nums[i] - k exists in the current subset.

Algorithm Implementation:

The DFS function processes each index i in the array:

  1. Base Case: When i >= len(nums), we've made decisions for all elements, so we increment ans by 1 (counting this valid subset).

  2. Two Choices at Each Step:

    • Skip the current element: Simply call dfs(i + 1) to move to the next element without including nums[i].
    • Include the current element (if valid):
      • First check if cnt[nums[i] + k] == 0 and cnt[nums[i] - k] == 0
      • If both are zero (meaning neither nums[i] + k nor nums[i] - k is in the current subset), we can safely include nums[i]
      • Increment cnt[nums[i]] by 1 to mark its inclusion
      • Recursively call dfs(i + 1) to process remaining elements
      • Backtrack by decrementing cnt[nums[i]] to restore the state

Key Implementation Details:

  • Initial ans = -1: This clever initialization accounts for the empty subset that the DFS will count when all elements are skipped. Since we need non-empty subsets only, starting at -1 gives us the correct count.

  • Counter Usage: The Counter automatically handles non-existent keys by returning 0, making the constraint check clean: cnt[nums[i] + k] == 0 and cnt[nums[i] - k] == 0.

  • Backtracking Pattern: The increment-recurse-decrement pattern (cnt[nums[i]] += 1, dfs(i + 1), cnt[nums[i]] -= 1) ensures we properly explore all possibilities while maintaining correct state.

Time Complexity: O(2^n) where n is the length of nums, as we explore all possible subsets.

Space Complexity: O(n) for the recursion stack and the counter in the worst case.

Ready to land your dream job?

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

Start Evaluator

Example Walkthrough

Let's trace through the algorithm with nums = [2, 5, 9] and k = 3.

We start with ans = -1 and an empty counter cnt = {}.

Initial call: dfs(0) (considering nums[0] = 2)

  • First, skip 2: call dfs(1)

    • At index 1 (nums[1] = 5):
      • Skip 5: call dfs(2)
        • At index 2 (nums[2] = 9):
          • Skip 9: call dfs(3) → base case, ans = 0 (subset: [])
          • Include 9: Check cnt[12] == 0 and cnt[6] == 0
            • Set cnt[9] = 1, call dfs(3) → base case, ans = 1 (subset: [9])
            • Backtrack: cnt[9] = 0
      • Include 5: Check cnt[8] == 0 and cnt[2] == 0
        • Set cnt[5] = 1, call dfs(2)
          • At index 2 (nums[2] = 9):
            • Skip 9: call dfs(3) → base case, ans = 2 (subset: [5])
            • Include 9: Check cnt[12] == 0 and cnt[6] == 0
              • Set cnt[9] = 1, call dfs(3) → base case, ans = 3 (subset: [5, 9])
              • Backtrack: cnt[9] = 0
        • Backtrack: cnt[5] = 0
  • Now include 2: Check cnt[5] == 0 and cnt[-1] == 0

    • Set cnt[2] = 1, call dfs(1)
      • At index 1 (nums[1] = 5):
        • Skip 5: call dfs(2)
          • At index 2 (nums[2] = 9):
            • Skip 9: call dfs(3) → base case, ans = 4 (subset: [2])
            • Include 9: Check cnt[12] == 0 and cnt[6] == 0
              • Set cnt[9] = 1, call dfs(3) → base case, ans = 5 (subset: [2, 9])
              • Backtrack: cnt[9] = 0
        • Include 5: Check cnt[8] == 0 and cnt[2] == 0 ✗ (cnt[2] = 1)
          • Cannot include 5 when 2 is already in the subset (|5 - 2| = 3 = k)
      • Backtrack: cnt[2] = 0

Final answer: 5 (representing the non-empty beautiful subsets: [2], [5], [9], [2, 9], [5, 9])

The algorithm correctly avoided creating [2, 5] since their difference equals k, and counted all other valid subsets.

Solution Implementation

1class Solution:
2    def beautifulSubsets(self, nums: List[int], k: int) -> int:
3        """
4        Count the number of beautiful subsets of nums.
5        A subset is beautiful if no two elements differ by exactly k.
6      
7        Args:
8            nums: List of integers to form subsets from
9            k: The difference to avoid between elements
10          
11        Returns:
12            Number of non-empty beautiful subsets
13        """
14        def backtrack(index: int) -> None:
15            """
16            Recursively generate all possible subsets and count valid ones.
17          
18            Args:
19                index: Current position in nums array
20            """
21            nonlocal beautiful_subset_count
22          
23            # Base case: reached end of array, increment count
24            if index >= len(nums):
25                beautiful_subset_count += 1
26                return
27          
28            # Choice 1: Skip current element
29            backtrack(index + 1)
30          
31            # Choice 2: Include current element if it doesn't violate the beautiful condition
32            # Check if adding nums[index] would create a pair with difference k
33            if frequency_map[nums[index] + k] == 0 and frequency_map[nums[index] - k] == 0:
34                # Include current element in subset
35                frequency_map[nums[index]] += 1
36                backtrack(index + 1)
37                # Backtrack: remove current element from subset
38                frequency_map[nums[index]] -= 1
39      
40        # Initialize count to -1 to exclude empty subset
41        beautiful_subset_count = -1
42      
43        # Track frequency of elements in current subset being built
44        frequency_map = Counter()
45      
46        # Start backtracking from index 0
47        backtrack(0)
48      
49        return beautiful_subset_count
50
1class Solution {
2    // Global variables for the DFS approach
3    private int[] nums;           // Input array of numbers
4    private int[] count;          // Frequency array to track elements in current subset
5    private int result;           // Count of beautiful subsets
6    private int k;                // The difference value to avoid
7  
8    /**
9     * Counts the number of beautiful subsets in the given array.
10     * A beautiful subset is one where no two elements have an absolute difference of k.
11     * 
12     * @param nums The input array of integers
13     * @param k The forbidden difference between elements
14     * @return The number of non-empty beautiful subsets
15     */
16    public int beautifulSubsets(int[] nums, int k) {
17        this.nums = nums;
18        this.k = k;
19        this.count = new int[1010];  // Frequency array (assumes nums[i] < 1010)
20        this.result = -1;             // Initialize to -1 to exclude empty subset
21      
22        dfs(0);
23      
24        return result;
25    }
26  
27    /**
28     * Depth-first search to generate all possible subsets and count beautiful ones.
29     * Uses backtracking to explore including/excluding each element.
30     * 
31     * @param index Current index being processed in the nums array
32     */
33    private void dfs(int index) {
34        // Base case: processed all elements, found a valid subset
35        if (index >= nums.length) {
36            result++;
37            return;
38        }
39      
40        // Option 1: Skip current element (exclude from subset)
41        dfs(index + 1);
42      
43        // Option 2: Include current element if it maintains beautiful property
44        // Check if adding nums[index] would violate the beautiful subset condition
45        int currentValue = nums[index];
46      
47        // Check if currentValue + k exists in current subset
48        boolean noConflictWithLarger = (currentValue + k >= count.length) || 
49                                       (count[currentValue + k] == 0);
50      
51        // Check if currentValue - k exists in current subset
52        boolean noConflictWithSmaller = (currentValue - k < 0) || 
53                                        (count[currentValue - k] == 0);
54      
55        // Only include current element if it doesn't conflict with existing elements
56        if (noConflictWithLarger && noConflictWithSmaller) {
57            // Include current element in subset
58            count[currentValue]++;
59            dfs(index + 1);
60            // Backtrack: remove current element from subset
61            count[currentValue]--;
62        }
63    }
64}
65
1class Solution {
2public:
3    int beautifulSubsets(vector<int>& nums, int k) {
4        // Initialize count of beautiful subsets (start from -1 to exclude empty set)
5        int beautifulSubsetCount = -1;
6      
7        // Frequency array to track which numbers are currently in the subset
8        // Size 1010 to handle the constraint range
9        int frequency[1010]{};
10      
11        int n = nums.size();
12
13        // Recursive DFS function to generate all possible subsets
14        auto dfs = [&](this auto&& dfs, int index) -> void {
15            // Base case: reached the end of array, found a valid subset
16            if (index >= n) {
17                ++beautifulSubsetCount;
18                return;
19            }
20          
21            // Option 1: Skip current element and continue
22            dfs(index + 1);
23          
24            // Option 2: Try to include current element if it's valid
25            // Check if adding current element maintains the "beautiful" property
26          
27            // Check if (current + k) exists in subset (avoid out of bounds)
28            bool noConflictWithLarger = (nums[index] + k >= 1010) || 
29                                        (frequency[nums[index] + k] == 0);
30          
31            // Check if (current - k) exists in subset (avoid negative index)
32            bool noConflictWithSmaller = (nums[index] - k < 0) || 
33                                         (frequency[nums[index] - k] == 0);
34          
35            // Include current element only if no conflicts exist
36            if (noConflictWithLarger && noConflictWithSmaller) {
37                // Add current element to subset
38                ++frequency[nums[index]];
39              
40                // Recurse with current element included
41                dfs(index + 1);
42              
43                // Backtrack: remove current element from subset
44                --frequency[nums[index]];
45            }
46        };
47      
48        // Start DFS from index 0
49        dfs(0);
50      
51        return beautifulSubsetCount;
52    }
53};
54
1/**
2 * Counts the number of beautiful subsets in the given array.
3 * A beautiful subset is one where no two elements have an absolute difference equal to k.
4 * 
5 * @param nums - The input array of numbers
6 * @param k - The difference value to check against
7 * @returns The count of beautiful subsets (excluding empty subset)
8 */
9function beautifulSubsets(nums: number[], k: number): number {
10    // Initialize result counter (starts at -1 to exclude empty subset)
11    let beautifulSubsetCount: number = -1;
12  
13    // Frequency array to track which elements are currently in the subset
14    // Size 1010 to handle constraint range
15    const elementFrequency: number[] = new Array(1010).fill(0);
16  
17    // Store array length for efficiency
18    const arrayLength: number = nums.length;
19  
20    /**
21     * Depth-first search to explore all possible subsets
22     * @param currentIndex - Current position in the nums array
23     */
24    const exploreSubsets = (currentIndex: number): void => {
25        // Base case: reached end of array, found a valid subset
26        if (currentIndex >= arrayLength) {
27            beautifulSubsetCount++;
28            return;
29        }
30      
31        // Option 1: Skip current element
32        exploreSubsets(currentIndex + 1);
33      
34        // Check if we can include current element
35        // Verify no element with difference +k exists in current subset
36        const noConflictWithLarger: boolean = 
37            nums[currentIndex] + k >= 1010 || elementFrequency[nums[currentIndex] + k] === 0;
38      
39        // Verify no element with difference -k exists in current subset
40        const noConflictWithSmaller: boolean = 
41            nums[currentIndex] - k < 0 || elementFrequency[nums[currentIndex] - k] === 0;
42      
43        // Option 2: Include current element if no conflicts
44        if (noConflictWithLarger && noConflictWithSmaller) {
45            // Add element to current subset
46            elementFrequency[nums[currentIndex]]++;
47          
48            // Continue exploring with this element included
49            exploreSubsets(currentIndex + 1);
50          
51            // Backtrack: remove element from subset
52            elementFrequency[nums[currentIndex]]--;
53        }
54    };
55  
56    // Start DFS from index 0
57    exploreSubsets(0);
58  
59    return beautifulSubsetCount;
60}
61

Time and Space Complexity

Time Complexity: O(2^n), where n is the length of the array nums.

The algorithm uses a depth-first search (DFS) approach to explore all possible subsets. At each index i, we have two choices:

  1. Skip the current element (proceed to dfs(i + 1))
  2. Include the current element if it doesn't violate the beautiful subset condition (check if nums[i] ± k are not in the current subset)

Since we make 2 decisions at each of the n positions, the total number of recursive calls is 2^n. Each recursive call performs constant time operations (checking the counter and updating it), so the overall time complexity is O(2^n).

Space Complexity: O(n)

The space complexity consists of:

  1. Recursion call stack: The maximum depth of recursion is n (when we've processed all elements), contributing O(n) space.
  2. Counter dictionary (cnt): In the worst case, we might have all n distinct elements in our subset at once, requiring O(n) space.
  3. Other variables: The ans variable uses O(1) space.

Therefore, the total space complexity is O(n).

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

Common Pitfalls

1. Forgetting to Handle Duplicate Elements Correctly

The most common pitfall is not realizing that the array can contain duplicate elements, and the Counter approach handles this naturally. However, developers might incorrectly try to use a Set instead of a Counter, which would fail for arrays with duplicates.

Incorrect approach:

# Using a set instead of Counter - WRONG!
current_subset = set()
if (nums[index] + k) not in current_subset and (nums[index] - k) not in current_subset:
    current_subset.add(nums[index])
    backtrack(index + 1)
    current_subset.remove(nums[index])  # This fails if nums has duplicates!

Why this fails: If nums = [1, 3, 1] and k = 2, when we try to include both 1's, the set would only store one instance, and removing would incorrectly remove the element entirely when backtracking from the second 1.

Correct solution: Use Counter to track frequencies:

frequency_map = Counter()
frequency_map[nums[index]] += 1  # Correctly handles multiple occurrences
backtrack(index + 1)
frequency_map[nums[index]] -= 1  # Properly decrements count

2. Incorrect Initialization of Answer Variable

Developers often initialize ans = 0 forgetting that the backtracking will count the empty subset (when all elements are skipped).

Incorrect:

beautiful_subset_count = 0  # WRONG - will include empty subset in final count

Correct:

beautiful_subset_count = -1  # Compensates for the empty subset

3. Using Set Operations Instead of Index-Based Backtracking

Some might try to generate all subsets first using bit manipulation or itertools, then filter:

Inefficient approach:

from itertools import combinations
count = 0
for r in range(1, len(nums) + 1):
    for subset in combinations(nums, r):
        if is_beautiful(subset, k):
            count += 1

Why this is problematic:

  • Doesn't handle duplicate elements correctly (combinations works with positions, not values)
  • Less efficient as it generates all subsets before checking
  • More complex to implement the beautiful check

Better solution: The index-based backtracking naturally handles duplicates and prunes invalid branches early.

Discover Your Strengths and Weaknesses: Take Our 5-Minute Quiz to Tailor Your Study Plan:

What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?


Recommended Readings

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

Load More