title: Partition to K Equal Sum Subsets
slug: partition-to-k-equal-sum-subsets
difficulty: medium
leetcode_id: 698
leetcode_url: https://leetcode.com/problems/partition-to-k-equal-sum-subsets/
categories:
  - arrays
  - dynamic-programming
  - recursion
patterns:
  - slug: backtracking
    is_optimal: true

function_signature: "def can_partition_k_subsets(nums: list[int], k: int) -> bool:"

test_cases:
  visible:
    - input: { nums: [4, 3, 2, 3, 5, 2, 1], k: 4 }
      expected: true
    - input: { nums: [1, 2, 3, 4], k: 3 }
      expected: false
  hidden:
    - input: { nums: [1], k: 1 }
      expected: true
    - input: { nums: [1, 1, 1, 1], k: 4 }
      expected: true
    - input: { nums: [1, 1, 1, 1], k: 2 }
      expected: true
    - input: { nums: [2, 2, 2, 2, 3, 4, 5], k: 4 }
      expected: false
    - input: { nums: [10, 10, 10, 7, 7, 7, 7, 7, 7, 6, 6, 6], k: 3 }
      expected: true

description: |
  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.

constraints: |
  - `1 <= k <= nums.length <= 16`
  - `1 <= nums[i] <= 10^4`
  - The frequency of each element is in the range `[1, 4]`

examples:
  - 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 of 5 each."
  - input: "nums = [1,2,3,4], k = 3"
    output: "false"
    explanation: "The total sum is 10, which is not divisible by 3, so equal partition is impossible."

explanation:
  intuition: |
    Imagine you have `k` empty buckets, and each bucket must hold exactly the same total weight. Your task is to distribute all the items (numbers) from the array into these buckets such that every bucket ends up with the same sum.

    The first insight is mathematical: if we can partition the array into `k` equal-sum subsets, then each subset must have sum equal to `total_sum / k`. If the total sum isn't divisible by `k`, we can immediately return `false`.

    Think of it like this: you're trying to fill `k` buckets one by one. For each number, you try placing it in a bucket. If a bucket would overflow (exceed the target sum), you skip it. If you successfully fill all buckets to exactly the target, you've found a valid partition.

    The key constraint `nums.length <= 16` is a hint — this small size allows exponential-time solutions like backtracking. The challenge is to prune the search space efficiently to avoid exploring redundant paths.

  approach: |
    We solve this using **Backtracking with Pruning**:

    **Step 1: Early validation**

    - Calculate `total_sum` of all elements
    - If `total_sum % k != 0`, return `false` immediately — equal partition is impossible
    - Calculate `target = total_sum / k` — this is the sum each bucket must have
    - If any single element exceeds `target`, return `false` — it can't fit in any bucket

    &nbsp;

    **Step 2: Sort in descending order**

    - Sort `nums` in descending order
    - This crucial optimisation places larger numbers first, failing faster when a bucket can't fit
    - Large numbers have fewer placement options, so trying them early prunes more branches

    &nbsp;

    **Step 3: Backtracking with k buckets**

    - Create an array `buckets` of size `k`, all initialised to `0`
    - For each number (in descending order), try placing it in each bucket
    - A number can go in bucket `i` if `buckets[i] + num <= target`
    - If placement succeeds, recursively try to place the next number
    - If all numbers are placed successfully, return `true`
    - If no valid placement exists, backtrack (remove number from bucket) and try the next bucket

    &nbsp;

    **Step 4: Skip duplicate buckets**

    - If `buckets[i] == buckets[i-1]`, skip bucket `i` — placing the number there would lead to the same configuration we'll explore via bucket `i-1`
    - This avoids redundant work when buckets have identical current sums

    &nbsp;

    **Step 5: Return the result**

    - If backtracking explores all options without success, return `false`

  common_pitfalls:
    - title: Missing the Divisibility Check
      description: |
        If `total_sum % k != 0`, it's mathematically impossible to partition into `k` equal subsets. Always check this first to avoid unnecessary computation.

        For example, `nums = [1,2,3,4]` with `k = 3` has sum `10`, which isn't divisible by `3`.
      wrong_approach: "Starting backtracking without checking divisibility"
      correct_approach: "Return false immediately if total_sum % k != 0"

    - title: Not Sorting in Descending Order
      description: |
        Without sorting, the backtracking explores many dead-end paths before failing. By placing larger numbers first, we fail faster when a number can't fit.

        Consider `nums = [1,1,1,1,1,1,10]` with `k = 2` and `target = 8`. Without sorting, we might try many combinations of 1s before realising the `10` can never fit. Sorting puts `10` first, causing immediate failure.
      wrong_approach: "Backtracking on unsorted array"
      correct_approach: "Sort descending to prune earlier"

    - title: Not Skipping Duplicate Bucket States
      description: |
        If two buckets have the same current sum, placing a number in either leads to equivalent configurations. Without this optimisation, the algorithm does redundant work.

        For example, with `buckets = [3, 3, 0, 0]` and trying to place `2`, placing it in bucket 0 vs bucket 1 creates the same branching structure. Skip one to avoid duplication.
      wrong_approach: "Trying every bucket regardless of current sums"
      correct_approach: "Skip bucket i if buckets[i] == buckets[i-1]"

    - title: Forgetting the Single Large Element Check
      description: |
        If any element is larger than `target`, it can never fit in any bucket. Check this after calculating the target to fail fast.
      wrong_approach: "Letting backtracking discover this through exhaustion"
      correct_approach: "Check max(nums) <= target before backtracking"

  key_takeaways:
    - "**Backtracking pattern**: When exploring combinations, try each option, recurse, then undo (backtrack) if it doesn't lead to a solution"
    - "**Pruning is essential**: Sorting descending and skipping duplicate states transform an unusable algorithm into a practical one"
    - "**Mathematical pre-checks**: Always validate constraints (divisibility, max element) before starting expensive computation"
    - "**Small constraints hint at approach**: `n <= 16` suggests exponential algorithms like backtracking or bitmask DP are acceptable"

  time_complexity: "O(k * 2^n). In the worst case, we explore all possible subsets for each of the k buckets. Pruning significantly reduces this in practice."
  space_complexity: "O(n). We use O(n) for the recursion stack depth and O(k) for the buckets array."

solutions:
  - approach_name: Backtracking with Pruning
    is_optimal: true
    code: |
      def can_partition_k_subsets(nums: list[int], k: int) -> bool:
          total = sum(nums)

          # If total isn't divisible by k, equal partition is impossible
          if total % k != 0:
              return False

          target = total // k

          # If any element exceeds target, it can't fit in any bucket
          if max(nums) > target:
              return False

          # Sort descending to fail faster (large numbers have fewer options)
          nums.sort(reverse=True)

          # k buckets, each starting empty
          buckets = [0] * k

          def backtrack(index: int) -> bool:
              # All numbers placed successfully
              if index == len(nums):
                  return True

              num = nums[index]

              for i in range(k):
                  # Skip duplicate bucket states to avoid redundant work
                  if i > 0 and buckets[i] == buckets[i - 1]:
                      continue

                  # Try placing num in bucket i
                  if buckets[i] + num <= target:
                      buckets[i] += num

                      # Recurse to place next number
                      if backtrack(index + 1):
                          return True

                      # Backtrack: remove num from bucket
                      buckets[i] -= num

              # No valid placement found for this number
              return False

          return backtrack(0)
    explanation: |
      **Time Complexity:** O(k * 2^n) worst case — we may explore all subset combinations. Pruning makes this much faster in practice.

      **Space Complexity:** O(n) — recursion stack depth plus O(k) for buckets array.

      We try to fill k buckets by placing each number (largest first) into a valid bucket. Key optimisations: sorting descending to fail fast, skipping duplicate bucket states to avoid redundant paths, and early termination checks.

  - approach_name: Bitmask Dynamic Programming
    is_optimal: false
    code: |
      def can_partition_k_subsets(nums: list[int], k: int) -> bool:
          total = sum(nums)

          if total % k != 0:
              return False

          target = total // k
          n = len(nums)

          # dp[mask] = remaining sum needed for current bucket
          # -1 means this state is unreachable
          dp = [-1] * (1 << n)
          dp[0] = 0  # Empty set has 0 sum

          for mask in range(1 << n):
              if dp[mask] == -1:
                  continue

              for i in range(n):
                  # If element i is not yet used
                  if mask & (1 << i) == 0:
                      # Current bucket sum after adding nums[i]
                      new_sum = dp[mask] + nums[i]

                      if new_sum <= target:
                          new_mask = mask | (1 << i)
                          # Mod by target: when bucket fills, start new one
                          dp[new_mask] = new_sum % target

          # All elements used, last bucket exactly filled
          return dp[(1 << n) - 1] == 0
    explanation: |
      **Time Complexity:** O(n * 2^n) — we iterate through all 2^n subsets and for each check n elements.

      **Space Complexity:** O(2^n) — the dp array stores state for each possible subset.

      This approach uses bitmask DP where each bit represents whether an element is used. We track the "remaining space" in the current bucket. When a bucket fills (`sum == target`), we start fresh with the next bucket (achieved via `% target`). This guarantees we consider all valid partitions systematically.
