Facebook Pixel

698. Partition to K Equal Sum Subsets

Problem Description

You are given an integer array nums and an integer k. Your task is to determine whether it's possible to divide the array into exactly k non-empty subsets where each subset has the same sum.

For example, if nums = [4, 3, 2, 3, 5, 2, 1] and k = 4, you can divide it into 4 subsets: [5], [1, 4], [2, 3], and [2, 3], where each subset sums to 5. In this case, you would return true.

The key requirements are:

  • You must use all elements from the array
  • Each element can only belong to one subset
  • You need exactly k subsets (no more, no less)
  • All k subsets must have equal sums

The solution uses a depth-first search approach with backtracking. First, it checks if the total sum of the array is divisible by k - if not, it's impossible to create equal subsets. If divisible, the target sum for each subset is total_sum / k. The algorithm then tries to place each element into one of the k buckets (subsets), backtracking when a placement doesn't lead to a valid solution. The array is sorted in descending order to optimize the search by trying larger elements first, which helps identify invalid paths earlier.

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 partitioning an array into subsets, not traversing nodes and edges in a graph structure.

Need to solve for kth smallest/largest?

  • No: We're not looking for the kth smallest or largest element. We need to determine if a valid partition exists.

Involves Linked Lists?

  • No: The problem works with an integer array, not linked list operations.

Does the problem have small constraints?

  • Yes: Looking at typical constraints for this problem, we have:
    • Array length is usually limited (often ≤ 16)
    • k is typically small (often ≤ 4)
    • These small constraints make exhaustive search feasible

Brute force / Backtracking?

  • Yes: Given the small constraints and the need to explore different ways of partitioning elements into k subsets, backtracking is appropriate. We need to:
    • Try placing each element in different subsets
    • Backtrack when a placement doesn't lead to a valid solution
    • Explore all possible assignments until we find a valid partition or exhaust all options

Conclusion: The flowchart correctly leads us to use a Backtracking approach. This aligns perfectly with the solution, which uses DFS with backtracking to try different element placements in k subsets, undoing choices that don't work and exploring alternative paths.

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

Intuition

The key insight is recognizing this as a partitioning problem where we need to make decisions about where each element belongs. Think of it like having k buckets and n items - we need to distribute all items such that each bucket has the same weight.

First, we need to check if equal partitioning is even possible. If the total sum isn't divisible by k, we can immediately return false. If it is divisible, each subset must have sum equal to total_sum / k.

Now, how do we assign elements to subsets? We can think of this as a decision tree where at each level, we decide which subset to place the current element in. For each element, we have up to k choices (which subset to add it to). This naturally leads to a backtracking approach - we try placing an element in subset 1, then recursively try to place the remaining elements. If that doesn't work, we backtrack and try subset 2, and so on.

To optimize our search:

  1. Sort in descending order: Larger elements are more restrictive in where they can be placed. By trying them first, we can fail fast on impossible configurations.
  2. Skip duplicate states: If two subsets currently have the same sum, trying to add the current element to either will lead to equivalent sub-problems. We only need to try one.
  3. Prune invalid branches: If adding an element to a subset would exceed the target sum s, we don't explore that branch.

The beauty of this approach is that we're essentially building all k subsets simultaneously, making one placement decision at a time and verifying constraints as we go. When we successfully place all elements (reach the base case), we know we've found a valid partition.

Learn more about Memoization, Dynamic Programming, Backtracking and Bitmask patterns.

Solution Approach

Let's walk through the implementation step by step, following the DFS + Pruning approach outlined in the reference solution.

Step 1: Check Feasibility

s, mod = divmod(sum(nums), k)
if mod:
    return False

First, we calculate the total sum and check if it's divisible by k. If there's a remainder (mod != 0), it's impossible to create equal subsets, so we return false immediately. The variable s stores the target sum for each subset.

Step 2: Initialize Data Structures

cur = [0] * k
nums.sort(reverse=True)
  • cur: An array of size k tracking the current sum of each subset. Initially all zeros.
  • We sort nums in descending order to try larger elements first, which helps prune the search tree earlier.

Step 3: DFS with Backtracking

def dfs(i: int) -> bool:
    if i == len(nums):
        return True

The recursive function dfs(i) tries to place element at index i into one of the k subsets. Base case: if we've successfully placed all elements (i == len(nums)), we've found a valid partition.

Step 4: Try Each Subset

for j in range(k):
    if j and cur[j] == cur[j - 1]:
        continue

For the current element nums[i], we try placing it in each subset j. The optimization if j and cur[j] == cur[j - 1] skips redundant attempts - if subset j has the same sum as subset j-1, trying to add the current element to either produces equivalent sub-problems.

Step 5: Backtrack Logic

cur[j] += nums[i]
if cur[j] <= s and dfs(i + 1):
    return True
cur[j] -= nums[i]
  • Add nums[i] to subset j
  • Only continue if the subset sum doesn't exceed target s
  • Recursively try to place the next element
  • If successful, propagate True up the call stack
  • Otherwise, backtrack by removing nums[i] from subset j and try the next subset

Step 6: Return Result

return False

If we've tried all subsets for the current element and none worked, return false. The main function returns the result of dfs(0), starting from the first element.

The algorithm essentially builds a decision tree where each level represents choosing a subset for an element, with pruning based on sum constraints and duplicate states.

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 walk through a small example with nums = [4, 3, 2, 1] and k = 2.

Step 1: Check Feasibility

  • Total sum = 4 + 3 + 2 + 1 = 10
  • 10 ÷ 2 = 5 with remainder 0 ✓
  • Target sum per subset: s = 5

Step 2: Initialize

  • Sort array descending: nums = [4, 3, 2, 1]
  • Create buckets: cur = [0, 0] (two empty subsets)

Step 3: DFS Process

Starting with dfs(0) - placing element nums[0] = 4:

  • Try subset 0: cur[0] = 0 + 4 = 4 (≤ 5, valid)
  • cur = [4, 0]
  • Recursively call dfs(1)

At dfs(1) - placing element nums[1] = 3:

  • Try subset 0: cur[0] = 4 + 3 = 7 (> 5, invalid - skip)
  • Try subset 1: cur[1] = 0 + 3 = 3 (≤ 5, valid)
  • cur = [4, 3]
  • Recursively call dfs(2)

At dfs(2) - placing element nums[2] = 2:

  • Try subset 0: cur[0] = 4 + 2 = 6 (> 5, invalid - skip)
  • Try subset 1: cur[1] = 3 + 2 = 5 (= 5, valid)
  • cur = [4, 5]
  • Recursively call dfs(3)

At dfs(3) - placing element nums[3] = 1:

  • Try subset 0: cur[0] = 4 + 1 = 5 (= 5, valid)
  • cur = [5, 5]
  • Recursively call dfs(4)

At dfs(4):

  • i == len(nums) - all elements placed successfully!
  • Return True

The algorithm successfully partitions the array into:

  • Subset 0: {4, 1} with sum = 5
  • Subset 1: {3, 2} with sum = 5

Each recursive call returns True, propagating the success back to the main function, which returns True.

Solution Implementation

1class Solution:
2    def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
3        """
4        Determine if array can be partitioned into k subsets with equal sum.
5      
6        Args:
7            nums: List of positive integers
8            k: Number of subsets to partition into
9      
10        Returns:
11            True if valid partition exists, False otherwise
12        """
13      
14        def backtrack(index: int) -> bool:
15            """
16            Recursively try to assign each number to one of k buckets.
17          
18            Args:
19                index: Current index in nums array being processed
20          
21            Returns:
22                True if valid assignment exists from current state
23            """
24            # Base case: all numbers have been successfully assigned
25            if index == len(nums):
26                return True
27          
28            # Try placing current number in each bucket
29            for bucket_idx in range(k):
30                # Optimization: skip duplicate bucket states
31                # If current bucket has same sum as previous, skip to avoid redundant computation
32                if bucket_idx > 0 and bucket_sums[bucket_idx] == bucket_sums[bucket_idx - 1]:
33                    continue
34              
35                # Add current number to this bucket
36                bucket_sums[bucket_idx] += nums[index]
37              
38                # If bucket sum doesn't exceed target and recursive call succeeds
39                if bucket_sums[bucket_idx] <= target_sum and backtrack(index + 1):
40                    return True
41              
42                # Backtrack: remove current number from this bucket
43                bucket_sums[bucket_idx] -= nums[index]
44          
45            # No valid placement found for current number
46            return False
47      
48        # Calculate target sum for each subset
49        total_sum = sum(nums)
50        target_sum, remainder = divmod(total_sum, k)
51      
52        # If total sum not divisible by k, partition impossible
53        if remainder != 0:
54            return False
55      
56        # Initialize k buckets with sum 0
57        bucket_sums = [0] * k
58      
59        # Sort in descending order to fail early for impossible cases
60        nums.sort(reverse=True)
61      
62        # Start backtracking from first element
63        return backtrack(0)
64
1class Solution {
2    private int[] numbers;           // Original input array
3    private int[] bucketSums;        // Current sum for each subset/bucket
4    private int targetSum;            // Target sum for each subset
5  
6    /**
7     * Determines if the array can be partitioned into k subsets with equal sum
8     * @param nums the input array of positive integers
9     * @param k the number of subsets to partition into
10     * @return true if partitioning is possible, false otherwise
11     */
12    public boolean canPartitionKSubsets(int[] nums, int k) {
13        // Calculate total sum of all elements
14        int totalSum = 0;
15        for (int num : nums) {
16            totalSum += num;
17        }
18      
19        // Check if total sum is divisible by k
20        if (totalSum % k != 0) {
21            return false;
22        }
23      
24        // Calculate target sum for each subset
25        targetSum = totalSum / k;
26      
27        // Initialize bucket array to track sum of each subset
28        bucketSums = new int[k];
29      
30        // Sort array in ascending order for optimization
31        Arrays.sort(nums);
32        this.numbers = nums;
33      
34        // Start DFS from the largest element (last index after sorting)
35        return dfs(nums.length - 1);
36    }
37  
38    /**
39     * Recursive DFS to try placing elements into different buckets
40     * @param elementIndex current element index to be placed
41     * @return true if valid partition exists, false otherwise
42     */
43    private boolean dfs(int elementIndex) {
44        // Base case: all elements have been successfully placed
45        if (elementIndex < 0) {
46            return true;
47        }
48      
49        // Try placing current element in each bucket
50        for (int bucketIndex = 0; bucketIndex < bucketSums.length; bucketIndex++) {
51            // Optimization: skip buckets with same sum to avoid duplicate states
52            if (bucketIndex > 0 && bucketSums[bucketIndex] == bucketSums[bucketIndex - 1]) {
53                continue;
54            }
55          
56            // Try placing element in current bucket
57            bucketSums[bucketIndex] += numbers[elementIndex];
58          
59            // Check if placement is valid and recursively try next element
60            if (bucketSums[bucketIndex] <= targetSum && dfs(elementIndex - 1)) {
61                return true;
62            }
63          
64            // Backtrack: remove element from current bucket
65            bucketSums[bucketIndex] -= numbers[elementIndex];
66        }
67      
68        // No valid placement found for current element
69        return false;
70    }
71}
72
1class Solution {
2public:
3    bool canPartitionKSubsets(vector<int>& nums, int k) {
4        // Calculate total sum of all elements
5        int totalSum = accumulate(nums.begin(), nums.end(), 0);
6      
7        // If total sum is not divisible by k, we cannot partition
8        if (totalSum % k != 0) {
9            return false;
10        }
11      
12        // Calculate target sum for each subset
13        int targetSum = totalSum / k;
14        int n = nums.size();
15      
16        // Track current sum of each subset
17        vector<int> subsetSums(k, 0);
18      
19        // Recursive function to try placing each number into k subsets
20        function<bool(int)> backtrack = [&](int index) -> bool {
21            // Base case: all numbers have been successfully placed
22            if (index == n) {
23                return true;
24            }
25          
26            // Try placing current number in each subset
27            for (int subsetIdx = 0; subsetIdx < k; ++subsetIdx) {
28                // Optimization: Skip duplicate states
29                // If current subset has same sum as previous, skip to avoid redundant computation
30                if (subsetIdx > 0 && subsetSums[subsetIdx] == subsetSums[subsetIdx - 1]) {
31                    continue;
32                }
33              
34                // Try adding current number to this subset
35                subsetSums[subsetIdx] += nums[index];
36              
37                // If subset sum doesn't exceed target, continue with next number
38                if (subsetSums[subsetIdx] <= targetSum && backtrack(index + 1)) {
39                    return true;
40                }
41              
42                // Backtrack: remove current number from this subset
43                subsetSums[subsetIdx] -= nums[index];
44            }
45          
46            // Could not place current number in any valid subset
47            return false;
48        };
49      
50        // Sort numbers in descending order for optimization
51        // Larger numbers are harder to place, so we try them first
52        sort(nums.begin(), nums.end(), greater<int>());
53      
54        // Start backtracking from first number
55        return backtrack(0);
56    }
57};
58
1/**
2 * Determines if array nums can be partitioned into k equal sum subsets
3 * @param nums - Array of positive integers to partition
4 * @param k - Number of subsets to create
5 * @returns true if nums can be partitioned into k equal sum subsets, false otherwise
6 */
7function canPartitionKSubsets(nums: number[], k: number): boolean {
8    // Calculate total sum and check if it's divisible by k
9    let totalSum: number = nums.reduce((accumulator, current) => accumulator + current, 0);
10  
11    // If total sum is not divisible by k, equal partitions are impossible
12    if (totalSum % k !== 0) {
13        return false;
14    }
15  
16    // Calculate target sum for each subset
17    const targetSum: number = totalSum / k;
18  
19    // Array to track current sum of each subset
20    const currentSubsetSums: number[] = Array(k).fill(0);
21  
22    // Sort array in descending order for optimization (try larger numbers first)
23    nums.sort((a, b) => b - a);
24  
25    /**
26     * Recursive DFS function to try placing each number into subsets
27     * @param index - Current index in nums array being processed
28     * @returns true if valid partition is found from this state
29     */
30    const depthFirstSearch = (index: number): boolean => {
31        // Base case: all numbers have been successfully placed
32        if (index === nums.length) {
33            return true;
34        }
35      
36        // Try placing current number in each subset
37        for (let subsetIndex = 0; subsetIndex < k; subsetIndex++) {
38            // Optimization: skip duplicate empty subsets
39            // If previous subset has same sum, no need to try again
40            if (subsetIndex > 0 && currentSubsetSums[subsetIndex] === currentSubsetSums[subsetIndex - 1]) {
41                continue;
42            }
43          
44            // Add current number to this subset
45            currentSubsetSums[subsetIndex] += nums[index];
46          
47            // If subset sum doesn't exceed target and recursive call succeeds
48            if (currentSubsetSums[subsetIndex] <= targetSum && depthFirstSearch(index + 1)) {
49                return true;
50            }
51          
52            // Backtrack: remove current number from this subset
53            currentSubsetSums[subsetIndex] -= nums[index];
54        }
55      
56        // No valid placement found for current number
57        return false;
58    };
59  
60    // Start DFS from first element
61    return depthFirstSearch(0);
62}
63

Time and Space Complexity

Time Complexity: O(k^n)

The algorithm uses a depth-first search approach where at each level of recursion (corresponding to placing each number), we try to place the current number in one of k buckets. In the worst case:

  • We have n numbers to place (depth of recursion tree is n)
  • At each level, we try up to k different buckets
  • This gives us a recursion tree with branching factor k and depth n
  • Total nodes in the recursion tree: k^n

The pruning optimization if j and cur[j] == cur[j - 1]: continue helps skip redundant states when consecutive buckets have the same sum, but this doesn't change the worst-case complexity.

Space Complexity: O(n)

The space complexity consists of:

  • Recursion call stack: O(n) - maximum depth is n when we process all numbers
  • The cur array: O(k) - stores current sum for each subset
  • Since k ≤ n (we need at least k elements to form k non-empty subsets), and the recursion stack dominates, the overall space complexity is O(n)

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

Common Pitfalls

Pitfall 1: Forgetting to Check if Any Single Element Exceeds Target Sum

The Problem: A common mistake is not checking whether any single element in the array is greater than the target sum before starting the backtracking process. If nums = [10, 5, 5, 2] and k = 3, the target sum would be 22/3 = 7.33.... While the division check catches this case, consider nums = [10, 5, 5] and k = 2 where target sum is 10. The element 10 alone equals the target, but without proper validation, the algorithm might waste time trying combinations.

Solution: Add an early termination check after sorting:

# After sorting in descending order
nums.sort(reverse=True)

# Check if largest element exceeds target
if nums[0] > target_sum:
    return False

Pitfall 2: Not Understanding Why Sorting in Descending Order Matters

The Problem: Developers often skip sorting or sort in ascending order, thinking it doesn't matter. However, sorting in descending order is crucial for performance. When we place larger elements first, we can identify invalid paths much earlier. For example, if a large element doesn't fit in any bucket, we know immediately rather than after trying many smaller elements first.

Solution: Always sort in descending order and understand that this optimization can reduce time complexity from timeout to acceptable:

# This is critical for performance
nums.sort(reverse=True)

Pitfall 3: Incorrect Duplicate State Detection

The Problem: The line if bucket_idx > 0 and bucket_sums[bucket_idx] == bucket_sums[bucket_idx - 1]: is often misunderstood or implemented incorrectly. Some developers might try to use a set to track seen sums or implement more complex duplicate detection, which either doesn't work correctly or adds unnecessary overhead.

Solution: The simple consecutive comparison works because:

  • We're trying buckets in order (0, 1, 2, ...)
  • If bucket j has the same sum as bucket j-1, placing the current element in either creates identical subproblems
  • This only works because we process buckets sequentially in the loop

Pitfall 4: Not Handling Edge Cases

The Problem: Missing edge cases like:

  • k = 1 (should return True for any valid array)
  • k > len(nums) (impossible to create k non-empty subsets)
  • Empty array or single element cases

Solution: Add edge case handling at the beginning:

def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
    # Edge cases
    if k == 1:
        return True
    if k > len(nums):
        return False
    if len(nums) == 0:
        return False
  
    # Continue with main logic...

Pitfall 5: Attempting Optimization with Memoization Incorrectly

The Problem: Some developers try to add memoization using the bucket sums as state, like @cache or using a dictionary with tuple(bucket_sums) as key. This often fails because:

  1. The bucket sums array changes through backtracking
  2. The order of buckets matters during the recursive process
  3. The state space can still be very large

Solution: Instead of memoization, rely on the existing optimizations:

  • Sorting in descending order
  • Skipping duplicate bucket states
  • Early termination when bucket sum exceeds target

If memoization is needed, use a bitmask to represent which elements have been used:

@cache
def dfs(used_mask: int, completed_subsets: int) -> bool:
    # Different approach using bitmask for used elements
    pass
Discover Your Strengths and Weaknesses: Take Our 3-Minute Quiz to Tailor Your Study Plan:

In a binary min heap, the maximum element can be found in:


Recommended Readings

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

Load More