questions M-R

This commit is contained in:
2025-05-25 12:43:25 +01:00
parent ad320dc703
commit 0a0feb93b5
62 changed files with 12841 additions and 0 deletions

View File

@@ -0,0 +1,220 @@
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
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.