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.
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:
- 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.
- 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.
- 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 sizek
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 subsetj
- 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 subsetj
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 EvaluatorExample 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 isn
) - At each level, we try up to
k
different buckets - This gives us a recursion tree with branching factor
k
and depthn
- 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 isn
when we process all numbers - The
cur
array:O(k)
- stores current sum for each subset - Since
k ≤ n
(we need at leastk
elements to formk
non-empty subsets), and the recursion stack dominates, the overall space complexity isO(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 bucketj-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:
- The bucket sums array changes through backtracking
- The order of buckets matters during the recursive process
- 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
In a binary min heap, the maximum element can be found in:
Recommended Readings
Memoization Prereq Backtracking problems backtracking Memoization is a fancy word for a simple concept so is the case for a lot of things we learn in school It means saving the previous function call result in a dictionary and reading from it when we do the exact same call again
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!