Files
codetutor/backend/data/questions/matchsticks-to-square.yaml

248 lines
11 KiB
YAML

title: Matchsticks to Square
slug: matchsticks-to-square
difficulty: medium
leetcode_id: 473
leetcode_url: https://leetcode.com/problems/matchsticks-to-square/
categories:
- arrays
- recursion
patterns:
- slug: backtracking
is_optimal: true
function_signature: "def makesquare(matchsticks: list[int]) -> bool:"
test_cases:
visible:
- input: { matchsticks: [1, 1, 2, 2, 2] }
expected: true
- input: { matchsticks: [3, 3, 3, 3, 4] }
expected: false
hidden:
- input: { matchsticks: [1, 1, 1, 1] }
expected: true
- input: { matchsticks: [1, 2, 3] }
expected: false
- input: { matchsticks: [5, 5, 5, 5, 4, 4, 4, 4, 3, 3, 3, 3] }
expected: true
- input: { matchsticks: [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] }
expected: false
- input: { matchsticks: [2, 2, 2, 2, 2, 2, 2, 2] }
expected: true
- input: { matchsticks: [10] }
expected: false
- input: { matchsticks: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] }
expected: true
description: |
You are given an integer array `matchsticks` where `matchsticks[i]` is the length of the i<sup>th</sup> matchstick. You want to use **all the matchsticks** to make one square. You **should not break** any stick, but you can link them up, and each matchstick must be used **exactly one time**.
Return `true` if you can make this square and `false` otherwise.
constraints: |
- `1 <= matchsticks.length <= 15`
- `1 <= matchsticks[i] <= 10^8`
examples:
- input: "matchsticks = [1,1,2,2,2]"
output: "true"
explanation: "You can form a square with length 2. One side of the square comes from two sticks with length 1."
- input: "matchsticks = [3,3,3,3,4]"
output: "false"
explanation: "You cannot find a way to form a square with all the matchsticks."
explanation:
intuition: |
Imagine you have a pile of matchsticks on a table, and you need to arrange them into four groups — one for each side of a square. Every matchstick must be used exactly once, and all four sides must have the **same total length**.
The first insight is mathematical: for a square to be possible, the total length of all matchsticks must be **divisible by 4**. If it isn't, there's no way to form four equal sides, so we can immediately return `false`.
The second insight is about problem structure: this is a **partition problem**. We need to partition the array into 4 subsets where each subset sums to `total_length / 4`. This is a classic backtracking scenario — we try placing each matchstick into one of the four sides, and if we hit a dead end, we backtrack and try a different placement.
Think of it like this: you pick up each matchstick and ask, "Which side should this go on?" You try the first side, and if you can eventually complete the square, great! If not, you take the matchstick back and try another side. This systematic exploration guarantees we find a solution if one exists.
approach: |
We solve this using **Backtracking with Pruning**:
**Step 1: Check if a square is possible**
- Calculate the total sum of all matchsticks
- If `total % 4 != 0`, return `false` immediately — a square is impossible
- Calculate `side_length = total // 4` — this is what each side must sum to
&nbsp;
**Step 2: Sort matchsticks in descending order (optimisation)**
- Sorting largest-first helps us fail faster
- Large matchsticks are harder to place, so if they can't fit, we discover this early
- This dramatically reduces the search space in practice
&nbsp;
**Step 3: Use backtracking to fill four sides**
- Create an array `sides = [0, 0, 0, 0]` to track the current length of each side
- For each matchstick, try adding it to each of the four sides
- If adding would exceed `side_length`, skip that side (pruning)
- If we successfully place all matchsticks, return `true`
- If no valid placement exists, backtrack by removing the matchstick and trying the next side
&nbsp;
**Step 4: Additional pruning**
- Skip duplicate side values: if `sides[i] == sides[i-1]` and we already failed with `sides[i-1]`, skip `sides[i]`
- Early termination: if the first matchstick is larger than `side_length`, return `false`
&nbsp;
**Step 5: Return the result**
- If the backtracking completes successfully (all matchsticks placed), return `true`
- If we exhaust all possibilities, return `false`
common_pitfalls:
- title: Forgetting the Divisibility Check
description: |
If the total sum of matchsticks isn't divisible by 4, no valid square exists. Always check this first.
For example, with `matchsticks = [1, 2, 3]`, the sum is `6`. Since `6 % 4 = 2 != 0`, we can immediately return `false` without any backtracking.
This simple check can save enormous computation time.
wrong_approach: "Start backtracking without checking divisibility"
correct_approach: "Return false early if total % 4 != 0"
- title: Not Sorting for Efficiency
description: |
Without sorting, you might try many small matchsticks first, build up partial sides, then discover a large matchstick doesn't fit anywhere. All that work is wasted.
By sorting in **descending order**, you try placing the largest (most constrained) matchsticks first. If they can't fit, you fail fast and prune massive branches of the search tree.
With `n = 15` matchsticks and `4^15` potential placements, pruning is essential.
wrong_approach: "Process matchsticks in original order"
correct_approach: "Sort descending to fail fast on large matchsticks"
- title: Treating This as a Simple DP Problem
description: |
While dynamic programming with bitmasks can solve this (tracking which matchsticks are used), the state space is `2^n * 4` which is manageable for `n <= 15`.
However, backtracking with good pruning is often simpler to implement and understand. Both approaches work, but backtracking with descending sort is typically faster in practice due to aggressive early termination.
wrong_approach: "Overcomplicating with bitmask DP when backtracking suffices"
correct_approach: "Use backtracking with sorting and pruning"
- title: Missing Duplicate Side Pruning
description: |
If two sides have the same current length and we tried (and failed) adding a matchstick to one, there's no point trying the other — the result will be identical.
For example, if `sides = [3, 3, 0, 0]` and adding the current matchstick to `sides[0]` leads to failure, adding it to `sides[1]` will fail the same way.
Skipping these duplicate attempts significantly reduces redundant work.
wrong_approach: "Try every side even when some have identical values"
correct_approach: "Skip sides with the same value as a previously failed attempt"
key_takeaways:
- "**Partition problems and backtracking**: When you need to divide elements into groups with constraints, backtracking systematically explores all possibilities"
- "**Pruning is essential**: Without optimisations like sorting and duplicate skipping, backtracking can be impractically slow"
- "**Early termination**: Simple checks (divisibility, single element too large) can eliminate entire problem instances instantly"
- "**Related problems**: This pattern appears in Partition Equal Subset Sum, Partition to K Equal Sum Subsets, and Fair Distribution of Cookies"
time_complexity: "O(4^n) in the worst case, where `n` is the number of matchsticks. Each matchstick can potentially go into any of the 4 sides. However, pruning (sorting, duplicate skipping, sum checks) dramatically reduces this in practice."
space_complexity: "O(n) for the recursion call stack depth, plus O(1) for the sides array."
solutions:
- approach_name: Backtracking with Pruning
is_optimal: true
code: |
def makesquare(matchsticks: list[int]) -> bool:
total = sum(matchsticks)
# A square needs 4 equal sides
if total % 4 != 0:
return False
side_length = total // 4
# Sort descending to try large matchsticks first (fail fast)
matchsticks.sort(reverse=True)
# Early check: if largest matchstick > side_length, impossible
if matchsticks[0] > side_length:
return False
# Track current length of each of the 4 sides
sides = [0, 0, 0, 0]
def backtrack(index: int) -> bool:
# Base case: all matchsticks placed successfully
if index == len(matchsticks):
return True
stick = matchsticks[index]
# Try placing this matchstick on each side
for i in range(4):
# Pruning: skip if this side would exceed target
if sides[i] + stick > side_length:
continue
# Pruning: skip duplicate sides (same current length)
if i > 0 and sides[i] == sides[i - 1]:
continue
# Place the matchstick
sides[i] += stick
# Recurse to place the next matchstick
if backtrack(index + 1):
return True
# Backtrack: remove the matchstick
sides[i] -= stick
# No valid placement found for this matchstick
return False
return backtrack(0)
explanation: |
**Time Complexity:** O(4^n) worst case — each matchstick can go into 4 sides. Pruning reduces this significantly in practice.
**Space Complexity:** O(n) — recursion depth equals the number of matchsticks.
We try placing each matchstick into one of four sides. Sorting largest-first means we quickly discover when a large matchstick can't fit anywhere. Skipping duplicate side values avoids redundant exploration. If we place all matchsticks without any side exceeding the target length, we've found a valid square.
- approach_name: Brute Force (No Pruning)
is_optimal: false
code: |
def makesquare(matchsticks: list[int]) -> bool:
total = sum(matchsticks)
if total % 4 != 0:
return False
side_length = total // 4
sides = [0, 0, 0, 0]
def backtrack(index: int) -> bool:
if index == len(matchsticks):
# Check if all sides equal
return all(s == side_length for s in sides)
stick = matchsticks[index]
for i in range(4):
if sides[i] + stick <= side_length:
sides[i] += stick
if backtrack(index + 1):
return True
sides[i] -= stick
return False
return backtrack(0)
explanation: |
**Time Complexity:** O(4^n) — without pruning, explores many redundant branches.
**Space Complexity:** O(n) — recursion depth.
This version works but lacks the critical optimisations. Without sorting, it may build partial solutions with small matchsticks only to fail later on large ones. Without duplicate pruning, it explores identical configurations multiple times. For the constraint `n <= 15`, this can be noticeably slower than the optimised version.