248 lines
11 KiB
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
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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
|
|
|
|
|
|
|
|
**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`
|
|
|
|
|
|
|
|
**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.
|