title: Partition Equal Subset Sum
slug: partition-equal-subset-sum
difficulty: medium
leetcode_id: 416
leetcode_url: https://leetcode.com/problems/partition-equal-subset-sum/
categories:
  - arrays
  - dynamic-programming
patterns:
  - dynamic-programming

function_signature: "def can_partition(nums: list[int]) -> bool:"

test_cases:
  visible:
    - input: { nums: [1, 5, 11, 5] }
      expected: true
    - input: { nums: [1, 2, 3, 5] }
      expected: false
    - input: { nums: [1, 2, 5] }
      expected: false
  hidden:
    - input: { nums: [1, 1] }
      expected: true
    - input: { nums: [2, 2, 1, 1] }
      expected: true
    - input: { nums: [100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 99, 97] }
      expected: false

description: |
  Given an integer array `nums`, return `true` *if you can partition the array into two subsets such that the sum of the elements in both subsets is equal* or `false` *otherwise*.

constraints: |
  - `1 <= nums.length <= 200`
  - `1 <= nums[i] <= 100`

examples:
  - input: "nums = [1,5,11,5]"
    output: "true"
    explanation: "The array can be partitioned as [1, 5, 5] and [11]."
  - input: "nums = [1,2,3,5]"
    output: "false"
    explanation: "The array cannot be partitioned into equal sum subsets."

explanation:
  intuition: |
    At first glance, this looks like a partitioning problem where we need to divide an array into two groups. But here's the key insight: if we can partition the array into two subsets with **equal sums**, then each subset must sum to exactly **half of the total array sum**.

    Think of it like balancing a scale. If the total weight is 22, each side needs exactly 11 to balance. This transforms our problem: instead of finding two equal subsets, we just need to find **one subset that sums to `total_sum / 2`**. The remaining elements automatically form the other subset.

    This is a classic **0/1 Knapsack** problem in disguise. Imagine you have a knapsack with capacity `total_sum / 2`. Each number in the array is an item you can either include or exclude. Can you fill the knapsack exactly?

    Two immediate observations help us prune:
    - If the total sum is **odd**, it's impossible to split into two equal integer sums — return `false` immediately
    - If any single element exceeds `total_sum / 2`, it can't fit in either subset — return `false`

  approach: |
    We use **Dynamic Programming** with a boolean array to track achievable sums.

    **Step 1: Calculate the target**

    - Compute `total_sum` of all elements
    - If `total_sum` is odd, return `false` immediately (can't split evenly)
    - Set `target = total_sum // 2`

    &nbsp;

    **Step 2: Initialise the DP array**

    - Create a boolean array `dp` of size `target + 1`
    - `dp[i]` represents: "Can we achieve sum `i` using some subset of numbers seen so far?"
    - Set `dp[0] = True` — we can always achieve sum 0 by selecting nothing

    &nbsp;

    **Step 3: Process each number**

    - For each `num` in the array, iterate **backwards** from `target` down to `num`
    - For each sum `j`, if `dp[j - num]` is `True`, then `dp[j]` becomes `True`
    - We iterate backwards to avoid using the same number twice in one iteration

    &nbsp;

    **Step 4: Return the result**

    - Return `dp[target]` — whether we can achieve exactly half the total sum

    &nbsp;

    The backward iteration is crucial: if we went forward, adding a number could affect later calculations in the same pass, effectively "using" the number multiple times.

  common_pitfalls:
    - title: Forward Iteration Leads to Reusing Elements
      description: |
        A subtle but critical bug occurs if you iterate forward through sums instead of backward.

        Consider `nums = [1, 2]` with `target = 3`. If we process `1` going forward:
        - `dp[1] = True` (we can make 1)
        - Then checking `dp[2]`: `dp[2 - 1] = dp[1] = True`, so `dp[2] = True`

        But wait — we've effectively used `1` twice! The backward iteration prevents this by ensuring we only consider sums achievable *before* adding the current number.
      wrong_approach: "Forward iteration: for j in range(num, target + 1)"
      correct_approach: "Backward iteration: for j in range(target, num - 1, -1)"

    - title: Forgetting the Odd Sum Early Exit
      description: |
        If the total sum is odd (e.g., 11), there's no way to split it into two equal integer parts. Without this check, the algorithm would waste time computing a DP table for an impossible target.

        Always check `if total_sum % 2 != 0: return False` before proceeding.
      wrong_approach: "Skipping the odd check and computing DP anyway"
      correct_approach: "Return False immediately when total_sum is odd"

    - title: Not Handling Large Single Elements
      description: |
        If any element exceeds `target`, it cannot be part of either subset that sums to `target`. While the DP naturally handles this (we skip sums less than `num`), an explicit check can provide an early exit.

        For `nums = [1, 2, 100]` with `total_sum = 103`, this is odd so we'd exit anyway. But for `nums = [1, 100, 1]` with `total_sum = 102` and `target = 51`, the `100` makes partitioning impossible.

  key_takeaways:
    - "**Subset sum is 0/1 Knapsack**: Recognise that finding a subset with a target sum is equivalent to the classic knapsack problem"
    - "**Transform the problem**: Instead of finding two equal subsets, find one subset summing to half the total — much simpler"
    - "**Backward DP iteration**: When each element can only be used once, iterate backwards to prevent double-counting"
    - "**Early pruning matters**: Odd total sums are impossible; check this first for an O(1) exit in many cases"

  time_complexity: "O(n × target) where `n` is the array length and `target = sum(nums) / 2`. We process each number once and update up to `target` sums."
  space_complexity: "O(target). We use a 1D DP array of size `target + 1`. This can be up to O(n × max_element / 2) = O(n × 50) = O(n × 50) in the worst case given the constraints."

solutions:
  - approach_name: 1D Dynamic Programming
    is_optimal: true
    code: |
      def can_partition(nums: list[int]) -> bool:
          total_sum = sum(nums)

          # Odd sum can't be split into two equal parts
          if total_sum % 2 != 0:
              return False

          target = total_sum // 2

          # dp[i] = True if we can achieve sum i with some subset
          dp = [False] * (target + 1)
          dp[0] = True  # Sum of 0 is always achievable (empty subset)

          for num in nums:
              # Iterate backwards to avoid using same num twice
              for j in range(target, num - 1, -1):
                  # If we could make (j - num), we can now make j
                  if dp[j - num]:
                      dp[j] = True

              # Early exit if we've found the target
              if dp[target]:
                  return True

          return dp[target]
    explanation: |
      **Time Complexity:** O(n × target) — For each of the n numbers, we potentially update target sums.

      **Space Complexity:** O(target) — Single array of size target + 1.

      This optimised 1D approach uses the insight that we only need the previous row's values, and by iterating backwards, we can update in place without a second array.

  - approach_name: 2D Dynamic Programming
    is_optimal: false
    code: |
      def can_partition(nums: list[int]) -> bool:
          total_sum = sum(nums)

          # Odd sum can't be split into two equal parts
          if total_sum % 2 != 0:
              return False

          target = total_sum // 2
          n = len(nums)

          # dp[i][j] = True if first i elements can sum to j
          dp = [[False] * (target + 1) for _ in range(n + 1)]

          # Base case: sum 0 is achievable with any number of elements
          for i in range(n + 1):
              dp[i][0] = True

          for i in range(1, n + 1):
              num = nums[i - 1]
              for j in range(1, target + 1):
                  # Don't include current number
                  dp[i][j] = dp[i - 1][j]

                  # Include current number if it fits
                  if j >= num:
                      dp[i][j] = dp[i][j] or dp[i - 1][j - num]

          return dp[n][target]
    explanation: |
      **Time Complexity:** O(n × target) — Same as 1D approach.

      **Space Complexity:** O(n × target) — Full 2D table.

      This classic 2D DP makes the state transitions clearer: for each element, we either include it (look at `dp[i-1][j-num]`) or exclude it (look at `dp[i-1][j]`). While easier to understand, it uses more memory than necessary.

  - approach_name: Recursive with Memoization
    is_optimal: false
    code: |
      def can_partition(nums: list[int]) -> bool:
          total_sum = sum(nums)

          if total_sum % 2 != 0:
              return False

          target = total_sum // 2
          memo = {}

          def dp(index: int, remaining: int) -> bool:
              # Base cases
              if remaining == 0:
                  return True
              if index >= len(nums) or remaining < 0:
                  return False

              # Check memo
              if (index, remaining) in memo:
                  return memo[(index, remaining)]

              # Try including or excluding current element
              result = (dp(index + 1, remaining - nums[index]) or
                        dp(index + 1, remaining))

              memo[(index, remaining)] = result
              return result

          return dp(0, target)
    explanation: |
      **Time Complexity:** O(n × target) — Each unique (index, remaining) state computed once.

      **Space Complexity:** O(n × target) for memoization + O(n) recursion stack.

      This top-down approach may be more intuitive for those who think recursively. At each index, we branch: either include the current number (subtract from remaining) or skip it. Memoization prevents recomputation of identical subproblems.
