🧠
Backtracking with Memoization (Bitmask DP)
💡 Memoization avoids recomputing states by caching results for subsets already checked, drastically improving performance over pure backtracking. It leverages bitmasking to represent subsets efficiently.
Intuition
Represent chosen elements as a bitmask and store results of subproblems to avoid repeated work.
Algorithm
- Calculate total sum and target per subset.
- Use a bitmask to represent which elements are used.
- Recursively try to fill buckets, caching results for each bitmask state.
- Return true if all elements are used and buckets are correctly filled.
💡 Memoization prunes the exponential search by remembering failed or successful states, avoiding duplicate work.
Recurrence:dp[mask] = true if there exists a subset of unused elements forming a bucket and dp[mask | subset] is true
from typing import List
def canPartitionKSubsets(nums: List[int], k: int) -> bool:
total = sum(nums)
if total % k != 0:
return False
target = total // k
n = len(nums)
nums.sort(reverse=True)
if nums[0] > target:
return False
memo = {}
def backtrack(used, curr_sum, count):
if count == k - 1:
return True
if curr_sum == target:
res = backtrack(used, 0, count + 1)
memo[used] = res
return res
if used in memo:
return memo[used]
for i in range(n):
if not (used & (1 << i)) and curr_sum + nums[i] <= target:
if backtrack(used | (1 << i), curr_sum + nums[i], count):
return True
memo[used] = False
return False
return backtrack(0, 0, 0)
# Driver code
if __name__ == '__main__':
print(canPartitionKSubsets([4,3,2,3,5,2,1], 4)) # True
print(canPartitionKSubsets([1,2,3,4], 3)) # False
Line Notes
memo = {}Cache results for each bitmask to avoid recomputation
if count == k - 1:If k-1 buckets formed, last bucket must be valid
if curr_sum == target:Start filling next bucket when current bucket is full
if not (used & (1 << i))Check if element i is unused in current mask
memo[used] = FalseMark current state as unsolvable to prune future calls
import java.util.Arrays;
import java.util.HashMap;
public class Solution {
private int[] nums;
private int target, n, k;
private HashMap<Integer, Boolean> memo = new HashMap<>();
public boolean canPartitionKSubsets(int[] nums, int k) {
this.nums = nums;
this.k = k;
int total = 0;
for (int num : nums) total += num;
if (total % k != 0) return false;
target = total / k;
n = nums.length;
Arrays.sort(nums);
// Reverse to descending order
for (int i = 0, j = n - 1; i < j; i++, j--) {
int temp = nums[i];
nums[i] = nums[j];
nums[j] = temp;
}
if (nums[0] > target) return false;
return backtrack(0, 0, 0);
}
private boolean backtrack(int used, int currSum, int count) {
if (count == k - 1) return true;
if (currSum == target) {
boolean res = backtrack(used, 0, count + 1);
memo.put(used, res);
return res;
}
if (memo.containsKey(used)) return memo.get(used);
for (int i = 0; i < n; i++) {
if ((used & (1 << i)) == 0 && currSum + nums[i] <= target) {
if (backtrack(used | (1 << i), currSum + nums[i], count)) return true;
}
}
memo.put(used, false);
return false;
}
public static void main(String[] args) {
Solution sol = new Solution();
System.out.println(sol.canPartitionKSubsets(new int[]{4,3,2,3,5,2,1}, 4)); // true
System.out.println(sol.canPartitionKSubsets(new int[]{1,2,3,4}, 3)); // false
}
}
Line Notes
private HashMap<Integer, Boolean> memo = new HashMap<>();Memoize results for bitmask states
if (count == k - 1) return true;Last bucket must be valid if others are formed
if (currSum == target)Move to next bucket when current bucket is full
if ((used & (1 << i)) == 0)Check if element i is unused
memo.put(used, false);Cache failure to prune future calls
#include <iostream>
#include <vector>
#include <algorithm>
#include <unordered_map>
using namespace std;
class Solution {
vector<int> nums;
int target, n, k;
unordered_map<int, bool> memo;
bool backtrack(int used, int currSum, int count) {
if (count == k - 1) return true;
if (currSum == target) {
bool res = backtrack(used, 0, count + 1);
memo[used] = res;
return res;
}
if (memo.count(used)) return memo[used];
for (int i = 0; i < n; i++) {
if (!(used & (1 << i)) && currSum + nums[i] <= target) {
if (backtrack(used | (1 << i), currSum + nums[i], count)) return true;
}
}
memo[used] = false;
return false;
}
public:
bool canPartitionKSubsets(vector<int>& nums_, int k_) {
nums = nums_;
k = k_;
n = nums.size();
int total = 0;
for (int num : nums) total += num;
if (total % k != 0) return false;
target = total / k;
sort(nums.rbegin(), nums.rend());
if (nums[0] > target) return false;
return backtrack(0, 0, 0);
}
};
int main() {
Solution sol;
vector<int> nums1 = {4,3,2,3,5,2,1};
cout << (sol.canPartitionKSubsets(nums1, 4) ? "true" : "false") << endl; // true
vector<int> nums2 = {1,2,3,4};
cout << (sol.canPartitionKSubsets(nums2, 3) ? "true" : "false") << endl; // false
return 0;
}
Line Notes
unordered_map<int, bool> memo;Memoize bitmask states to avoid recomputation
if (count == k - 1) return true;Last bucket must be valid if others are formed
if (currSum == target)Start filling next bucket when current bucket is full
if (!(used & (1 << i)))Check if element i is unused
memo[used] = false;Cache failure to prune future calls
function canPartitionKSubsets(nums, k) {
const total = nums.reduce((a,b) => a + b, 0);
if (total % k !== 0) return false;
const target = total / k;
const n = nums.length;
nums.sort((a,b) => b - a);
if (nums[0] > target) return false;
const memo = new Map();
function backtrack(used, currSum, count) {
if (count === k - 1) return true;
if (currSum === target) {
const res = backtrack(used, 0, count + 1);
memo.set(used, res);
return res;
}
if (memo.has(used)) return memo.get(used);
for (let i = 0; i < n; i++) {
if (((used >> i) & 1) === 0 && currSum + nums[i] <= target) {
if (backtrack(used | (1 << i), currSum + nums[i], count)) return true;
}
}
memo.set(used, false);
return false;
}
return backtrack(0, 0, 0);
}
// Test cases
console.log(canPartitionKSubsets([4,3,2,3,5,2,1], 4)); // true
console.log(canPartitionKSubsets([1,2,3,4], 3)); // false
Line Notes
const memo = new Map();Memoize bitmask states to avoid recomputation
if (count === k - 1) return true;Last bucket must be valid if others are formed
if (currSum === target)Start filling next bucket when current bucket is full
if (((used >> i) & 1) === 0)Check if element i is unused
memo.set(used, false);Cache failure to prune future calls
TimeO(n * 2^n) due to memoization over subsets
SpaceO(2^n) for memo and O(n) recursion stack
Memoization reduces repeated exploration of subsets, but all subsets may still be visited.
💡 For n=16, 2^16=65536 states, which is feasible with pruning and memoization.
Interview Verdict: Accepted and efficient for constraints; good interview approach.
Shows mastery of bitmask DP and memoization, a common FAANG technique.