You are given an integer array nums and an integer k, return true if it is possible to divide this array into k non-empty subsets whose sums are all equal.
Example 1:
Input: nums = [2,4,1,3,5], k = 3
Output: trueExplanation: Given array can be divided into three subsets [5], [2,3], [4,1].
Example 2:
Input: nums = [1,2,3,4], k = 3
Output: falseConstraints:
1 <= k <= nums.length <= 161 <= nums[i] <= 10,000[1, 4].Before attempting this problem, you should be comfortable with:
The problem asks whether we can divide the array into exactly k subsets, each with the same sum. First, we check if the total sum is divisible by k. If not, it's impossible. Otherwise, each subset must sum to target = total / k.
We use backtracking to try placing each number into one of the k subsets. Sorting the array in descending order helps us fail faster when a large number cannot fit. Once a subset reaches the target sum, we start building the next one. If we successfully build all k subsets, we return true.
k, return false.target = total / k.used to track which elements have been assigned.backtrack(i, k, subsetSum):k == 0, all subsets are formed, return true.subsetSum == target, start building the next subset with backtrack(0, k - 1, 0).i, try adding it if it doesn't exceed the target.backtrack(0, k, 0).class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
if sum(nums) % k != 0:
return False
nums.sort(reverse=True)
target = sum(nums) // k
used = [False] * len(nums)
def backtrack(i, k, subsetSum):
if k == 0:
return True
if subsetSum == target:
return backtrack(0, k - 1, 0)
for j in range(i, len(nums)):
if used[j] or subsetSum + nums[j] > target:
continue
used[j] = True
if backtrack(j + 1, k, subsetSum + nums[j]):
return True
used[j] = False
return False
return backtrack(0, k, 0)Where is the size of the array and is the number of subsets.
This approach extends the basic backtracking by adding a key pruning optimization. If we fail to place any element into an empty subset (when subsetSum == 0), we know that element cannot be placed anywhere, so the entire configuration is invalid. This avoids redundant exploration of branches that will never succeed.
k, return false.target = total / k.backtrack(i, k, subsetSum):k == 0, return true.subsetSum == target, recurse to build the next subset.i:target.subsetSum == 0 and we failed, return false immediately.backtrack(0, k, 0).class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
total = sum(nums)
if total % k != 0:
return False
nums.sort(reverse=True)
target = total // k
used = [False] * len(nums)
def backtrack(i, k, subsetSum):
if k == 0:
return True
if subsetSum == target:
return backtrack(0, k - 1, 0)
for j in range(i, len(nums)):
if used[j] or subsetSum + nums[j] > target:
continue
used[j] = True
if backtrack(j + 1, k, subsetSum + nums[j]):
return True
used[j] = False
if subsetSum == 0: # Pruning
return False
return False
return backtrack(0, k, 0)Where is the size of the array and is the number of subsets.
Instead of using a boolean array to track used elements, we can represent the state as a bitmask. Each bit indicates whether the corresponding element has been used. This representation is more compact and prepares us for memoization in later approaches.
k, return false.target = total / k.mask = (1 << n) - 1 where all bits are set (all elements available).backtrack(i, k, subsetSum, mask):k == 0, return true.subsetSum == target, start the next subset with backtrack(0, k - 1, 0, mask).j from index i:j is not set in mask or adding nums[j] exceeds target, skip.mask ^ (1 << j).subsetSum == 0 and we fail, return false (pruning).backtrack(0, k, 0, (1 << n) - 1).class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
total = sum(nums)
if total % k != 0:
return False
nums.sort(reverse=True)
target = total // k
n = len(nums)
def backtrack(i, k, subsetSum, mask):
if k == 0:
return True
if subsetSum == target:
return backtrack(0, k - 1, 0, mask)
for j in range(i, n):
if (mask & (1 << j)) == 0 or subsetSum + nums[j] > target:
continue
if backtrack(j + 1, k, subsetSum + nums[j], mask ^ (1 << j)):
return True
if subsetSum == 0:
return False
return False
return backtrack(0, k, 0, (1 << n) - 1)Where is the size of the array and is the number of subsets.
The bitmask from the previous approach naturally lends itself to memoization. Different orderings of element selection can lead to the same mask, so we cache results for each mask to avoid recomputation. This transforms the exponential backtracking into a more efficient dynamic programming solution.
k, return false.target = total / k.dp of size 2^n, initialized to null/undefined.backtrack(i, k, subsetSum, mask):dp[mask] is already computed, return it.k == 0, set dp[mask] = true and return.subsetSum == target, recurse for the next subset and cache.i:target.mask.subsetSum == 0.dp[mask] = false if no valid configuration found.backtrack(0, k, 0, (1 << n) - 1).class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
total = sum(nums)
if total % k != 0:
return False
nums.sort(reverse=True)
target = total // k
n = len(nums)
dp = [None] * (1 << n)
def backtrack(i, k, subsetSum, mask):
if dp[mask] != None:
return dp[mask]
if k == 0:
dp[mask] = True
return True
if subsetSum == target:
dp[mask] = backtrack(0, k - 1, 0, mask)
return dp[mask]
for j in range(i, n):
if (mask & (1 << j)) == 0 or subsetSum + nums[j] > target:
continue
if backtrack(j + 1, k, subsetSum + nums[j], mask ^ (1 << j)):
dp[mask] = True
return True
if subsetSum == 0:
dp[mask] = False
return dp[mask]
dp[mask] = False
return False
return backtrack(0, k, 0, (1 << n) - 1)Instead of recursion, we iterate through all possible mask values from 0 to 2^n - 1. For each valid state (reachable configuration), we try adding each unused element. The key insight is that dp[mask] stores the current sum modulo target. If we can reach the full mask with sum 0 (meaning all subsets are complete), we have a valid partition.
k, return false.target = total / k.dp array of size 2^n with -1, except dp[0] = 0.mask from 0 to 2^n - 1:dp[mask] == -1, this state is unreachable; skip it.i:i is not set and adding nums[i] doesn't exceed target:dp[mask | (1 << i)] = (dp[mask] + nums[i]) % target.true if dp[(1 << n) - 1] == 0, meaning all elements are used and the sum completes exactly.class Solution:
def canPartitionKSubsets(self, nums: List[int], k: int) -> bool:
total = sum(nums)
if total % k != 0:
return False
target = total // k
n = len(nums)
N = 1 << n
dp = [0] + [-1] * (N - 1)
for mask in range(N):
if dp[mask] == -1:
continue
for i in range(n):
if (mask & (1 << i)) == 0 and dp[mask] + nums[i] <= target:
dp[mask | (1 << i)] = (dp[mask] + nums[i]) % target
return dp[N - 1] == 0