Paper Cut into Minimum Number of Squares asks you to slice a rectangular sheet of paper into the fewest possible perfect squares. It looks like a simple greedy problem — and the greedy intuition is a great place to start — but the exact minimum needs dynamic programming.
Problem. Given a sheet of paper of size n x m, cut it into the minimum number of squares.
Every cut is straight, edge to edge, and every final piece must be a perfect square.
Example: n = 5, m = 3 → answer 4 (a 3x3 square, a 2x2 square, and two 1x1 squares).
The slow way first
The natural greedy move: repeatedly slice off the largest square that fits, whose side is the smaller of the two remaining dimensions, and count how many you take. For 5 x 3 that gives 3x3, then a 2x3 strip, then 2x2, then a 2x1 strip needing two 1x1 squares — total 4.
The trouble is that greedy is not always optimal. For a 5 x 6 sheet greedy takes 5, but the true minimum is 5... and there are larger sizes where greedy overshoots. So greedy is a fine warm-up and a sanity check, but to guarantee the minimum we have to try every cut.
The idea: try every cut and keep the best
Define dp(w, h) = the minimum number of squares for a w x h sheet. If w == h the sheet is already a square, so the answer is 1. Otherwise every solution is formed by one straight cut that splits the sheet into two sub-rectangles — either a vertical cut (into k x h and (w-k) x h) or a horizontal cut (into w x k and w x (h-k)). Recurse on both halves, add their answers, and take the minimum over all cut positions.
Memoizing dp(w, h) (with lru_cache) turns the exponential recursion into a polynomial DP over all w x h subproblems.
Walk through it
Step through the animation. First it shows the greedy slicing on the 5 x 3 grid: a 3x3 block, then 2x2, then two 1x1 cells — 4 squares. Then it resets and switches to the DP view, which tries every vertical and horizontal cut and confirms the exact minimum is 4.
Pseudocode
dp(w, h):
if w == h: # already a perfect square
return 1
best = w * h # worst case: all 1x1 squares
for k from 1 to w//2: # every vertical cut
best = min(best, dp(k, h) + dp(w - k, h))
for k from 1 to h//2: # every horizontal cut
best = min(best, dp(w, k) + dp(w, h - k))
return bestThe Python solution
from functools import lru_cache
def min_squares(w, h):
@lru_cache(maxsize=None)
def dp(w, h):
if w == h:
return 1
best = w * h
# try every vertical cut
for k in range(1, w // 2 + 1):
best = min(best, dp(k, h) + dp(w - k, h))
# try every horizontal cut
for k in range(1, h // 2 + 1):
best = min(best, dp(w, k) + dp(w, h - k))
return best
return dp(w, h)dp(w, h)returns the minimum squares for aw x hpiece, memoized so each subproblem is solved once.if w == his the base case — a square sheet is one piece.best = w * hseeds the answer with the worst case (cut everything into1x1squares).- The first loop tries every vertical cut at column
k, splitting intok x hand(w-k) x h. We only go up tow // 2because cutkand cutw - kare mirror images. - The second loop tries every horizontal cut at row
kthe same way. - We keep the smallest total over all cuts, and
lru_cachemakes the overlapping subproblems cheap.
Complexity
| Case | Time | Notes |
|---|---|---|
| Greedy (largest square) | O(n + m) (moderate) | fast but not always optimal |
| DP with memoization | O(n * m * (n + m)) (moderate) | all subrectangles x all cut positions |
O(n * m) (moderate)There are O(n * m) distinct w x h subproblems, and each tries O(n + m) cut positions, so the exact DP is O(n * m * (n + m)) with O(n * m) cache space. Greedy is far faster but can miss the optimum.
When this pattern shows up
Whenever a problem says "cut / partition / split a shape or sequence into the fewest pieces," think interval / partition DP: define the answer for a sub-piece, then try every place to make the single next cut and recurse on both halves. Matrix-chain multiplication, palindrome partitioning, and burst balloons are all the same shape.
Do not trust greedy here. Slicing the largest square each time feels optimal and often is, but for some rectangles it uses more squares than necessary. Only the all-cuts DP guarantees the true minimum.
Practice
Using the greedy rule on a 5 x 3 sheet, what is the side of the very first square you cut off, and what rectangle is left?
1. What is the base case of the dp(w, h) recursion?
2. Why does the DP try every cut position instead of just slicing the largest square?
3. Why do the loops only run up to w // 2 and h // 2?
4. What is the extra space used by the memoized DP?