279. Perfect Squares
Problem Statement: Given a number
n, return the fewest perfect squares that add up to it.
Input: n = 12
Output: 3
Picture it
This is Coin Change with the denominations handed to you by arithmetic rather than by the input. The coins are 1, 4, 9, 16, 25 and so on, every square up to n, and each may be used as often as you like.
So the same reasoning applies. To build n, one square has to be the last one added. If it is 9, the rest makes n - 9, and you want that rest to use as few squares as possible, which is the same question on a smaller number.
best(n) = 1 + the smallest best(n - square) over every square that fits
Recognising the restatement is most of the work here. Once it is Coin Change, the implementation is already known.
Note that greed fails just as it did there. For 12, taking the largest square first gives 9 + 1 + 1 + 1, four squares, while 4 + 4 + 4 is three.
How to think about it
Build an array of best counts from 0 up to n.
Zero needs no squares. Every other entry starts as unreachable, though here nothing actually is: every number can be made from 1s, so the answer always exists and no -1 case is needed. That is the one simplification over Coin Change.
For each value, try every square up to it and keep the smallest result.
Generating the squares as you go, with an inner loop over i while i * i is at most the value, avoids building a list first and makes the bound obvious: there are about the square root of n squares to try.
The number-theory shortcut is worth knowing because interviewers enjoy it. Lagrange's four-square theorem says every positive integer is the sum of at most four squares, and there are simple tests for whether the answer is 1, 2 or 4, leaving 3 otherwise. That gives an solution. It is a fine thing to mention; the dynamic programming version is the one to write, since it generalises and does not depend on recalling a theorem.
Walking through the example
Take n = 12.
- Value 1. Only
1fits, giving1 + best(0) = 1. - Value 4. The square
4gives1 + best(0) = 1, beating four1s. - Value 8. The square
4gives1 + best(4) = 2, which is4 + 4. - Value 9. The square
9gives1. - Value 12. The square
1gives1 + best(11); the square4gives1 + best(8) = 3; the square9gives1 + best(3) = 4, since3needs three1s. The best is3.
The answer is 3, from 4 + 4 + 4.
Step 5 shows the greedy failure plainly: starting from the largest square, 9, leads to four, while 4 leads to three.
The code
class Solution:
def numSquares(self, n: int) -> int:
best = [0] * (n + 1)
for value in range(1, n + 1):
fewest = value # the worst case is that many 1s
i = 1
while i * i <= value:
# this square last, plus the best way to make the rest
fewest = min(fewest, 1 + best[value - i * i])
i += 1
best[value] = fewest
return best[n]
Seeding fewest with value itself is a tidy touch: it is the count of using nothing but 1s, which is always achievable, so no unreachable marker is needed.
Time and space complexity
Time:
Every value from 1 to n is tried against roughly the square root of n squares.
Space:
The array of best counts.
Remember this
This is Coin Change with the squares as denominations, and greed fails here for the same reason: 12 is 4 + 4 + 4, not 9 + 1 + 1 + 1. Build upward, trying every square as the last one added. No unreachable case exists, since 1 is a square, so seeding each value with its own count of 1s is enough. time.