698. Partition to K Equal Sum Subsets
Problem Description
In this LeetCode problem, we are tasked with determining whether a given array of integers nums
can be divided into k
non-empty subsets such that the sum of the numbers in each subset is equal. This problem is a variation of the classic partition problem which is known to be NP-complete, implying there is no known polynomial-time solution to solve it for the worst-case scenario. However, through clever approaches and optimizations, we can handle moderately sized inputs effectively.
To clarify:
- A non-empty subset means each subset must contain at least one integer from
nums
. - All
k
subsets must have the same sum, and together they must include all elements fromnums
.
If such a division is possible, we should return true
, otherwise false
.
Flowchart Walkthrough
To analyze the problem statement of Leetcode 698. Partition to K Equal Sum Subsets using the algorithm flowchart, let's break it down step by step:
- Is it a graph?
- No: This problem does not involve graphs in the classic sense of nodes and edges.
- Need to solve for kth smallest/largest?
- No: The objective is not about finding the kth smallest or largest element but about dividing the set into k subsets of equal sum.
- Involves Linked Lists?
- No: This problem is strictly dealing with arrays or sets, not linked-list data structures.
- Does the problem have small constraints?
- Yes: The challenge mostly lies in the method of partitioning, not in large data handling. The subsets and the recursive nature of trying each possibility fit well into problems with smaller, manageable constraints.
- Brute force / Backtracking?
- Yes: To find if we can partition the set into k subsets that all have equal sum, we often need to exhaustively try different combinations of numbers into each subset. This sort of problem is typically handled effectively with a backtracking approach where we recursively try to fill each subset and backtrack if we reach a configuration that doesn't work.
Hence, by following the flowchart, we arrive at the suitable pattern:
- Brute force / Backtracking: The problem requires exploring all possible ways to partition the array into subsets which qualify the given conditions, making backtracking an apt choice.
Conclusion: The flowchart indicates using a backtracking approach for solving Leetcode 698, as it perfectly aligns with the criteria of handling smaller constraints by exploring possible groupings.
Intuition
The intuition behind solving this problem generally involves backtracking, a brute-force technique where we build subsets element by element and backtrack if we reach an invalid state. However, we can optimize our search with a few strategies:
-
Pre-calculate subset sum (
s
): Since we aim for each of thek
subsets to have an equal sum, we first compute the total sum of the array and divide it byk
to find the target sum for each subset. If this division leaves a remainder (mod
), we can immediately returnfalse
, because it means the total sum cannot be evenly divided intok
parts. -
Sort and reverse: By sorting
nums
in descending order, we try larger numbers first. This is helpful because if larger numbers can't be placed into any subset, we'll fail faster and backtrack earlier, without wasting time on trying to fit smaller numbers first. -
Skip duplicate combinations: To avoid evaluating the same combination multiple times, we compare the current subset sum (
cur[j]
) with the previous one (cur[j - 1]
). If they're equal and we're about to consider the same number again, we continue with the loop to try a different placement. -
State space tree: Use a depth-first search (DFS) backtracking algorithm to explore each state. The function
dfs(i)
takes an indexi
and tries to putnums[i]
into each subset one by one. It recurses to check if addingnums[i]
to a subset will lead to a solution (dfs(i + 1)
). -
Backtracking: If adding
nums[i]
to the current subset doesn't lead to a solution or if the sum of the current subset exceeds the desired subset sum (s
), we backtrack by removingnums[i]
fromcur[j]
and try a different subset by continuing in the loop. -
Termination: The recursion terminates when all numbers are considered (
i == len(nums)
), at which point if we have not concluded to a contradiction, we returntrue
indicating a successful partition.
Using these optimizations, we can significantly reduce the number of combinations we need to try and therefore solve the problem more efficiently than the naive brute-force approach.
Learn more about Memoization, Dynamic Programming, Backtracking and Bitmask patterns.
Solution Approach
The solution for the problem is implemented as a method canPartitionKSubsets(nums, k)
in a Python class named Solution
. Here's a step-by-step walkthrough of how the code works:
-
Check divisibility: The sum of all elements in
nums
is divided byk
to check if it's possible to partition the array intok
subsets (s, mod = divmod(sum(nums), k)
). If there's a remainder (mod
is not0
), we returnfalse
immediately because an even partition is not possible. -
Data structure: A list (
cur
) is created withk
zeros (cur = [0] * k
), where each index represents the current sum of the corresponding subset. -
Sorting: The input list
nums
is sorted in reverse order (nums.sort(reverse=True)
). This allows us to consider larger elements first, which is beneficial for early backtracking. -
Depth-first search (DFS): The core of the solution is a recursive function
dfs(i)
which attempts to find the correct placement fornums[i]
in one of the subsets. The indexi
runs from0
tolen(nums) - 1
. -
Termination condition: If
i
equals the length ofnums
(i == len(nums)
), all items have been placed into some subset, and we returntrue
, as we've found a valid partition. -
Subset iteration: Inside the
dfs
function, we loop over each subset (for j in range(k):
), and try to placenums[i]
into thejth
subset. -
Duplicates check: To avoid placing the same combination again, we check if the current subset sum (
cur[j]
) is the same as the subset sum at the previous index (cur[j-1]
). If it is, andj
is not the first subset, we skip this iteration (continue
) to prevent redundant work. -
Placement trial: We add
nums[i]
to thejth
subset (cur[j] += nums[i]
) and check if the new sum is less than or equal to the target sum (s
). -
Recursion: If the new sum is valid, we call
dfs(i + 1)
to try and place the next element. -
Backtracking: If placing
nums[i]
into thejth
subset doesn't lead to a solution, or the new sum exceeds the target, we backtrack by removingnums[i]
fromcur[j]
(cur[j] -= nums[i]
) and try a different subset in the next iteration. -
Final call and return statement: The last line of the method (
return dfs(0)
) starts the recursive process and returns the final answer, indicating whether a valid partition intok
subsets with equal sums exists.
This algorithm employs backtracking, a form of DFS, to explore the solution space, and utilizes a few optimizations: sorting for fast backtracking, and duplicate combination skipping to reduce the state space.
Ready to land your dream job?
Unlock your dream job with a 2-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through an example to illustrate the solution approach with an array nums
and an integer k
. Suppose nums = [4, 3, 2, 3, 5, 2, 1]
and k = 4
. We need to find out if it's possible to partition this array into 4 subsets with equal sums.
-
Check divisibility: First, we find the sum of all elements in
nums
, which is4+3+2+3+5+2+1 = 20
. Then we divide 20 byk
(20 / 4
) and find that each subset must sum up to 5 (s = 5
), with no remainder, so an even partition is possible in theory. -
Data structure: We create a list
cur = [0, 0, 0, 0]
with 4 zeros, representing the sum of subsets we will form. -
Sorting: Sort
nums
in reverse to get[5, 4, 3, 3, 2, 2, 1]
. This allows us to fill the subsets starting with the largest numbers. -
Depth-first search (DFS): We define our DFS to try placing each element in
nums
into one of the subsets. -
Termination condition: If we reach the end of
nums
, placing all elements without contradictions, we can returntrue
. -
Subset iteration: We start with
nums[0]
, which is5
, and try to place it in the first subset.cur[0]
becomes5
, which is equal to our target subset sum (s
), so the first subset is complete. -
Duplicates check: As we iterate through the rest of
nums
, we make sure not to create subsets with the same sum in the same manner twice. -
Placement trial: We take the next number,
4
, and try adding it to our subsets, checking if the sum stays less than or equal tos
. It won't fit in the first subset as it's already full, but it fits in the second (subset sum becomes4
). -
Recursion: We iterate with the next element,
nums[2]
, and follow the previous steps. We keep trying until all elements are either placed or no placement is possible. -
Backtracking: If at any time, the current number doesn't fit in any subset, we backtrack, removing the last placed number and trying it in the next subset.
-
Final call and return statement: At the end, we reach a valid partition where each subset sums to 5 with subsets being
[5], [4, 1], [3, 2], [3, 2]
or any permutation of these. The function will now returntrue
, and our recursive process concludes that a valid partition is indeed possible.
In this example, with the help of sorting and careful placement of numbers, we minimized the number of operations required to find a valid partition. This demonstrates how the DFS backtracking algorithm efficiently solves the problem.
Solution Implementation
1from typing import List
2
3class Solution:
4 def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
5 # Helper function that tries to assign elements to subsets recursively
6 def search(subset_sums, index):
7 # If we've traversed all the numbers, check if all subsets are equal to target sum
8 if index == len(nums):
9 return True
10 # Try adding the current number to each subset in turn and recurse
11 for j in range(k):
12 # Optimization: Skip if the previous subset is identical to the current
13 if j > 0 and subset_sums[j] == subset_sums[j - 1]:
14 continue
15 subset_sums[j] += nums[index]
16 # Recurse only if the current subset sum is less than or equal to target subset sum
17 if subset_sums[j] <= target_sum and search(subset_sums, index + 1):
18 return True
19 # Backtrack: remove the number from the current subset and try the next one
20 subset_sums[j] -= nums[index]
21 # If no valid assignment found, return False
22 return False
23
24 # Calculate the target subset sum and check if it is possible to divide
25 total_sum, remainder = divmod(sum(nums), k)
26 if remainder:
27 return False
28
29 # Initialize k subset sums as 0
30 subset_sums = [0] * k
31 # Sort numbers in descending order to optimize the process
32 nums.sort(reverse=True)
33
34 # Call the recursive search function starting with index 0
35 return search(subset_sums, 0)
36
1import java.util.Arrays;
2
3public class Solution {
4 private int[] numbers; // Array of input numbers.
5 private int[] subsetSums; // Array to hold the sum of the current subsets.
6 private int targetSubsetSum; // The desired sum of each subset.
7
8 // Method to check if the numbers can be partitioned into k subsets with equal sum.
9 public boolean canPartitionKSubsets(int[] nums, int k) {
10 // Sum all the values in nums array.
11 int totalSum = Arrays.stream(nums).sum();
12
13 // If totalSum is not divisible by k, we cannot partition it into k subsets of equal sum.
14 if (totalSum % k != 0) {
15 return false;
16 }
17
18 // The target sum for each subset is the total sum divided by k.
19 this.targetSubsetSum = totalSum / k;
20
21 // Sort the array in ascending order to optimize backtracking (largest elements are placed first).
22 Arrays.sort(nums);
23
24 // Reverse the array to have the largest elements at the beginning.
25 for (int left = 0, right = nums.length - 1; left < right; left++, right--) {
26 int temp = nums[left];
27 nums[left] = nums[right];
28 nums[right] = temp;
29 }
30
31 // Initialize member variables.
32 this.numbers = nums;
33 this.subsetSums = new int[k];
34
35 // Start recursive DFS to find the solution.
36 return dfs(nums.length - 1);
37 }
38
39 // Recursive method using Depth-First Search to backtrack and find whether a valid partition exists.
40 private boolean dfs(int currentIndex) {
41 // If we've gone through all the elements, check if all subset sums are equal to the target sum.
42 if (currentIndex < 0) {
43 for (int sum : subsetSums) {
44 if (sum != targetSubsetSum) {
45 return false;
46 }
47 }
48 return true;
49 }
50
51 // Try to place the current element into each subset one by one.
52 for (int j = 0; j < subsetSums.length; ++j) {
53 // Skip duplicate situations where previous and current subsetSums are the same value (to avoid redundant work).
54 if (j > 0 && subsetSums[j] == subsetSums[j - 1]) {
55 continue;
56 }
57
58 // Add current element to the subset sum.
59 subsetSums[j] += numbers[currentIndex];
60
61 // If the new subset sum is less than or equal to the target,
62 // continue DFS with the next smaller element.
63 if (subsetSums[j] <= targetSubsetSum && dfs(currentIndex - 1)) {
64 return true;
65 }
66
67 // Backtrack: remove current element from the subset sum.
68 subsetSums[j] -= numbers[currentIndex];
69
70 // If the sum is zero after backtracking, no need to try placing the current element into later buckets.
71 if (subsetSums[j] == 0) break;
72 }
73
74 // If no valid placement was found, return false.
75 return false;
76 }
77}
78
1#include <vector>
2#include <numeric>
3#include <algorithm>
4#include <functional>
5
6class Solution {
7public:
8 bool canPartitionKSubsets(std::vector<int>& nums, int k) {
9 // Calculate the sum of all numbers
10 int totalSum = std::accumulate(nums.begin(), nums.end(), 0);
11
12 // If totalSum is not divisible by k, we can't partition it into k subsets.
13 if (totalSum % k) {
14 return false;
15 }
16
17 // The target sum for each subset.
18 int targetSum = totalSum / k;
19
20 // Sort numbers in descending order to help with the backtracking process.
21 std::sort(nums.begin(), nums.end(), std::greater<int>());
22
23 // Vector to store the current sum of each subset.
24 std::vector<int> subsetSums(k, 0);
25
26 // Define a depth-first search lambda to explore the possible partitions.
27 std::function<bool(int)> dfs = [&](int index) {
28 // If we have reached the end of nums, check if we successfully partitioned.
29 if (index == nums.size()) {
30 // All k subsets have our target sum.
31 return true;
32 }
33
34 // Iterate through each subset
35 for (int j = 0; j < k; ++j) {
36 // To avoid redundant work, if the previous subset has the same sum
37 // as the current one, we don't need to try nums[index] in this subset.
38 if (j > 0 && subsetSums[j] == subsetSums[j - 1]) {
39 continue;
40 }
41 // Add the current number to the current subset sum.
42 subsetSums[j] += nums[index];
43
44 // If the new sum does not exceed the target, and dfs succeeds, return true.
45 if (subsetSums[j] <= targetSum && dfs(index + 1)) {
46 return true;
47 }
48
49 // Backtrack: remove the current number from the current subset sum.
50 subsetSums[j] -= nums[index];
51 }
52 // If no valid partitioning is found, return false.
53 return false;
54 };
55
56 // Start the depth-first search with the first number.
57 return dfs(0);
58 }
59};
60
1function canPartitionKSubsets(nums: number[], k: number): boolean {
2 // Calculates the sum of all elements in nums
3 let totalSum = nums.reduce((accumulator, currentValue) => accumulator + currentValue);
4
5 // If the total sum is not divisible by k, it's not possible to partition the array
6 if (totalSum % k !== 0) {
7 return false;
8 }
9
10 // The target sum for each subset is the total sum divided by k
11 let targetSubsetSum = totalSum / k;
12
13 // Sort the array in ascending order to optimize the algorithm
14 nums.sort((a, b) => a - b);
15
16 // Get the length of nums
17 const n = nums.length;
18
19 // Initialize a boolean array to keep track of possible subset sums
20 const canFormSubsetSum = new Array(1 << n).fill(false);
21 canFormSubsetSum[0] = true;
22
23 // Initialize an array to store the current subset sum
24 const currentSubsetSum = new Array(n).fill(0);
25
26 // Iterate over all possible subsets
27 for (let i = 0; i < 1 << n; ++i) {
28 // If current subset cannot form the sum, skip to the next iteration
29 if (!canFormSubsetSum[i]) {
30 continue;
31 }
32
33 // Try to add each number to the current subset to form a new subset
34 for (let j = 0; j < n; ++j) {
35 // Skip if adding the number exceeds the target subset sum
36 if (currentSubsetSum[i] + nums[j] > targetSubsetSum) {
37 break;
38 }
39
40 // Check if the number is not already used in the current subset
41 if (((i >> j) & 1) === 0) {
42 const newSubsetIndex = i | (1 << j);
43 canFormSubsetSum[newSubsetIndex] = true; // Mark the new subset as possible
44 currentSubsetSum[newSubsetIndex] = (currentSubsetSum[i] + nums[j]) % targetSubsetSum;
45 }
46 }
47 }
48
49 // Check if there is a combination that includes all numbers forming k subsets
50 return canFormSubsetSum[(1 << n) - 1];
51}
52
Time and Space Complexity
The given Python code is trying to determine if it's possible to partition the input list nums
into k
subsets each with the same sum. The approach is using backtracking to explore all possible ways to create these partitions.
Time Complexity
The time complexity of this function can be quite high due to the nature of the problem, which is NP-complete.
The dfs
function tries to place each number into one of the k
subsets in a depth-first search manner. Given that there are n
numbers, for the first number there are k
choices, for the second number there are k
choices, and so on, leading to a total of k^n
possibilities in the worst case. However, there are some optimizations:
- The
for
loop in thedfs
function has pruning: it skips placing a number in a subset if the current subset has the same sum as the previous subset, which could reduce the number of explored branches. - Sorting the array in reverse order helps in an earlier detection of sums that exceed the target subset sum
s
, potentially reducing the number of recursive calls.
With such optimizations, the upper bound on the number of calls to dfs
is less than k^n
, but it is hard to quantify the exact impact without a specific case analysis.
Therefore, the time complexity is better represented as O(k^n)
, keeping in mind that this is a simplified upper bound and actual performance may be significantly better due to the mentioned optimizations.
Space Complexity
The space complexity is determined by:
- The depth of the recursion, which in the worst case is
O(n)
sincedfs
might go as deep as the number of elements in the list. - The space used by the list
cur
which isO(k)
.
Since these two factors are not dependent on each other, we add them up to get the total space complexity: O(n + k)
.
Remember that the recursion call stack space (for dfs
) has a space complexity of O(n)
because the depth of the recursion tree can go up to the number of elements in nums
. The additional O(k)
space is for maintaining the current sums of the k
subsets.
Learn more about how to find time and space complexity quickly using problem constraints.
Which technique can we use to find the middle of a linked list?
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!