{x}
blog image

Partition to K Equal Sum Subsets

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 = [4,3,2,3,5,2,1], k = 4
Output: true
Explanation: It is possible to divide it into 4 subsets (5), (1, 4), (2,3), (2,3) with equal sums.

Example 2:

Input: nums = [1,2,3,4], k = 3
Output: false

 

Constraints:

  • 1 <= k <= nums.length <= 16
  • 1 <= nums[i] <= 104
  • The frequency of each element is in the range [1, 4].

Solution Explanation for Partition to K Equal Sum Subsets

This problem asks whether a given integer array nums can be partitioned into k non-empty subsets, each with the same sum. The solution involves several approaches, each with its own trade-offs in terms of time and space complexity.

Core Idea: The fundamental approach across all solutions involves checking if the sum of elements in nums is divisible by k. If not, a partition into equal-sum subsets is impossible. If it is divisible, the problem reduces to finding k subsets with a sum of targetSum = sum(nums) / k.

Solution 1: DFS with Pruning

This solution uses Depth-First Search (DFS) with several optimizations for efficiency.

Approach:

  1. Preprocessing: Calculate targetSum. If sum(nums) is not divisible by k, return false. Sort nums in descending order to improve pruning efficiency (larger numbers are considered first).

  2. DFS function: The recursive dfs(index) function explores possible subset assignments. index tracks the current element being considered in nums.

  3. Iteration and Pruning: For each element, the algorithm attempts to add it to each of the k subsets (represented by an array cur of size k). Pruning is applied:

    • If adding the element exceeds targetSum, it's skipped for that subset.
    • If a subset's sum is equal to the previous subset's sum (cur[j] == cur[j-1]), it indicates that subset is complete, and the algorithm proceeds to the next one.
  4. Base Case: If index reaches the end of nums, it means all elements have been successfully assigned, so true is returned. Otherwise, false is returned.

Time Complexity: O(kn), where n is the length of nums. The worst-case scenario is trying all possible combinations of assigning elements to subsets. The sorting step is O(n log n). However, the pruning significantly reduces the search space in practice.

Space Complexity: O(n) due to the recursive call stack and the cur array.

Solution 2: State Compression with Memoization

This solution uses bit manipulation to represent the state of element assignment and memoization to avoid redundant calculations.

Approach:

  1. State Representation: A bitmask state (an integer) represents which elements have been included in subsets. The i-th bit being 1 means the i-th element is already assigned.

  2. Memoization: A cache (or f array) stores the results of subproblems (dfs(state, t)), where t is the current sum of the subset being built. cache[state] indicates whether it's possible to partition the remaining elements (represented by state) given the current subset sum.

  3. DFS Function: The dfs(state, t) function recursively explores states:

    • It checks the base case: if state represents all elements assigned (state == mask), it returns true.
    • It uses the cache to check if the result for the current state is already known.
    • For each unassigned element, it checks if adding it to the current subset (t + nums[i]) exceeds targetSum. If it does, it prunes the search for that element. Otherwise, it recursively calls dfs with the updated state and subset sum.

Time Complexity: O(n * 2n). The state space is 2n (all possible subsets), and for each state, we iterate through n elements.

Space Complexity: O(2n) for the cache (memoization table).

Solution 3: Dynamic Programming

This approach uses dynamic programming to efficiently determine the feasibility of partitioning.

Approach:

  1. State Definition: f[i] is a boolean array where f[i] = true means that there exists a way to partition the elements represented by the bitmask i into subsets with the desired sum. cur[i] stores the running sum for that state.

  2. Iteration: It iterates through all possible bitmasks i from 0 to 2n. For each bitmask, if f[i] is true, it means a valid partition for that state exists.

  3. Subset Update: For each unassigned element nums[j], it checks if adding it to the current subset exceeds targetSum. If not, and the element is not assigned ((i >> j) & 1 == 0), it sets f[i | (1 << j)] = true and updates cur[i | (1 << j)].

  4. Result: Finally, it checks f[(1 << n) - 1], which represents the state where all elements are assigned.

Time Complexity: O(n * 2n) due to iterating through all possible bitmasks and checking each element.

Space Complexity: O(2n) for the f and cur arrays.

Summary Table:

| Solution | Time Complexity | Space Complexity | |-------------------|----------------------|----------------------| | DFS with Pruning | O(kn) (worst case) | O(n) | | State Compression | O(n * 2n) | O(2n) | | Dynamic Programming | O(n * 2n) | O(2n) |

The constraint nums.length <= 16 makes the exponential time complexity of solutions 2 and 3 manageable. Solution 1, while potentially having a worse worst-case complexity, can be faster in practice due to pruning. The choice of the best solution depends on the specific input data and performance requirements.