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.
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:
- At each position, first try skipping the current element (this always works)
- Then check if we can include the current element by verifying the constraint
- 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 ifnums[i] + k
ornums[i] - k
exists in the current subset.
Algorithm Implementation:
The DFS function processes each index i
in the array:
-
Base Case: When
i >= len(nums)
, we've made decisions for all elements, so we incrementans
by 1 (counting this valid subset). -
Two Choices at Each Step:
- Skip the current element: Simply call
dfs(i + 1)
to move to the next element without includingnums[i]
. - Include the current element (if valid):
- First check if
cnt[nums[i] + k] == 0
andcnt[nums[i] - k] == 0
- If both are zero (meaning neither
nums[i] + k
nornums[i] - k
is in the current subset), we can safely includenums[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
- First check if
- Skip the current element: Simply call
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 EvaluatorExample 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
andcnt[6] == 0
✓- Set
cnt[9] = 1
, calldfs(3)
→ base case,ans = 1
(subset: [9]) - Backtrack:
cnt[9] = 0
- Set
- Skip 9: call
- At index 2 (nums[2] = 9):
- Include 5: Check
cnt[8] == 0
andcnt[2] == 0
✓- Set
cnt[5] = 1
, calldfs(2)
- At index 2 (nums[2] = 9):
- Skip 9: call
dfs(3)
→ base case,ans = 2
(subset: [5]) - Include 9: Check
cnt[12] == 0
andcnt[6] == 0
✓- Set
cnt[9] = 1
, calldfs(3)
→ base case,ans = 3
(subset: [5, 9]) - Backtrack:
cnt[9] = 0
- Set
- Skip 9: call
- At index 2 (nums[2] = 9):
- Backtrack:
cnt[5] = 0
- Set
- Skip 5: call
- At index 1 (nums[1] = 5):
-
Now include 2: Check
cnt[5] == 0
andcnt[-1] == 0
✓- Set
cnt[2] = 1
, calldfs(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
andcnt[6] == 0
✓- Set
cnt[9] = 1
, calldfs(3)
→ base case,ans = 5
(subset: [2, 9]) - Backtrack:
cnt[9] = 0
- Set
- Skip 9: call
- At index 2 (nums[2] = 9):
- Include 5: Check
cnt[8] == 0
andcnt[2] == 0
✗ (cnt[2] = 1
)- Cannot include 5 when 2 is already in the subset (|5 - 2| = 3 = k)
- Skip 5: call
- Backtrack:
cnt[2] = 0
- At index 1 (nums[1] = 5):
- Set
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:
- Skip the current element (proceed to
dfs(i + 1)
) - 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:
- Recursion call stack: The maximum depth of recursion is
n
(when we've processed all elements), contributingO(n)
space. - Counter dictionary (
cnt
): In the worst case, we might have alln
distinct elements in our subset at once, requiringO(n)
space. - Other variables: The
ans
variable usesO(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.
What is an advantages of top-down dynamic programming vs bottom-up dynamic programming?
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
Backtracking Template Prereq DFS with States problems dfs_with_states Combinatorial search problems Combinatorial search problems involve finding groupings and assignments of objects that satisfy certain conditions Finding all permutations combinations subsets and solving Sudoku are classic combinatorial problems The time complexity of combinatorial problems often grows rapidly with the size of
Want a Structured Path to Master System Design Too? Don’t Miss This!