Algorithm

Perfect Squares

Breadth-First Search (BFS) Pattern

Perfect Squares

Given a positive integer n, return the smallest count of perfect squares that add up to exactly n.

A perfect square is an integer multiplied by itself: 1, 4, 9, 16, 25 and so on. The same square may be used as many times as you like, and you must hit n exactly — not approach it. Since 1 is a perfect square, an answer always exists, so there is no impossible case.

CONSTRAINTS
  • 1 <= n <= 10⁴
  • Squares may be repeated freely.
  • Only the count is returned, not the squares themselves.
  • Order does not matter: 4 + 9 and 9 + 4 are the same answer of 2.
EXAMPLE 1
Input: n = 12
Output: 3
12 = 4 + 4 + 4 uses three squares. Nothing does it in two: the only squares below 12 are 1, 4 and 9, and none of the pairs 1+1, 1+4, 1+9, 4+4, 4+9 or 9+9 makes 12.
EXAMPLE 2
Input: n = 13
Output: 2
13 = 4 + 9. Note this is fewer than 12 needs despite 13 being larger — the answer does not grow with n, so greedily taking the biggest square first is not reliable.
EXAMPLE 3
Input: n = 1
Output: 1
1 is itself a perfect square, so a single term suffices.
EXAMPLE 4
Input: n = 7
Output: 4
7 = 4 + 1 + 1 + 1. Taking the largest square 4 leaves 3, which has no square below it except 1, forcing three more terms. No arrangement beats 4 — and no positive integer ever needs more than 4.
May I use the same perfect square more than once?
Yes. 12 = 4 + 4 + 4 is valid and is in fact the optimal answer. Without repetition the problem would be a different and much more restrictive one.
Do I return the count or the actual squares?
The count. Returning the squares themselves is a harder variant requiring you to record which choice produced each state.
Is there always a valid answer?
Always. 1 is a perfect square, so n copies of 1 is a fallback, meaning the answer is at most n and never impossible. The interesting question is only how far below n you can get.
Can I just take the largest square that fits, repeatedly?
No, and this is worth raising before you write any code. For 12 that greedy rule takes 9, leaving 3, then 1, 1, 1 — four terms, when three suffice. Locally optimal choices are not globally optimal here, which rules out a greedy solution and points at search or dynamic programming.

There is no grid here, no words, no puzzle — just a number. Which makes this a good place to test whether the shortest-path idea is really about grids, or about something more general.

Turning arithmetic into a path

Reword the question. You are standing on the number n. A move subtracts any perfect square that does not overshoot. From 12 you may move to 11 by subtracting 1, to 8 by subtracting 4, or to 3 by subtracting 9. Repeat until you land exactly on 0. The number of squares used is the number of moves made, so the fewest squares summing to n and the fewest moves from n down to 0 are the same question.

That is a graph, once you accept that a graph does not need to be drawn or stored:

- A node is an integer between 0 and n — a remaining amount still to be covered.
- An edge goes from x to x − s for each perfect square s that is at most x.
- Every edge costs exactly 1, because using any square counts as one term whether it is 1 or 9801.

That last property is what makes this tractable, and it is worth pausing on. The size of the square subtracted does not affect the price. A big jump and a small jump cost the same. So the problem is unweighted, and unweighted shortest paths are the easy kind.

Notice something else: the graph has at most n + 1 nodes and each has at most √n outgoing edges, since the squares that fit under n are 1², 2², … up to ⌊√n⌋². For n = 10⁴ that is 10,001 nodes with at most 100 edges each — about a million transitions. Entirely affordable. Sizing the space like this before choosing an algorithm tells you an exhaustive search is on the table.

Why the obvious recursion is too slow

Written directly, "try every square and take the best" is a few lines.

python
def num_squares(n):
    if n == 0:
        return 0
    best = n                                # worst case: n copies of 1
    s = 1
    while s * s <= n:
        best = min(best, 1 + num_squares(n - s * s))
        s += 1
    return best

Correct, and unusably slow. The reason is not the branching by itself but the repetition. Reaching 8 by subtracting 4 from 12, and reaching 8 by subtracting 1 from 9 which came from 10 which came from 12, are different histories arriving at the identical situation — and the identical situation has an identical answer. How you got to a number tells you nothing about the best way forward from it. The plain recursion re-derives the answer for 8 every time it arrives there, and the same for every other value, so the work multiplies exponentially.

That observation — the future depends only on the current state, never on the route taken to it — is exactly what licenses visiting each state once and only once. Which is what the next approach does.

Expanding outward in layers

Explore by distance rather than by depth. Layer 1 is every value reachable from n in one subtraction. Layer 2 is every unclaimed value reachable from layer 1. And so on. A value first appears in the layer equal to the minimum number of squares needed to get from n down to it — earlier is impossible, and later cannot happen because the moment a layer-d value is expanded, all of its unclaimed successors are claimed for layer d+1.

Consequently the first time 0 appears, the layer number is the answer, and the search stops. No comparing of alternatives, no best-so-far. Discovery order is answer order. This is Breadth-First Search, and it wants a queue — a structure returning values in the order they were found. FIFO ordering is what keeps layer d fully processed before layer d+1 begins; a stack would plunge down one chain of subtractions and the first arrival at 0 would be some answer rather than the smallest.

The other essential piece is a visited set, and here it is not merely an optimisation. Without it the search would revisit values endlessly and never finish in reasonable time. With it, each of the n+1 values is expanded at most once.

python
from collections import deque

def num_squares(n):
    squares = [i * i for i in range(1, int(n ** 0.5) + 1)]
    queue = deque([(n, 0)])                 # remaining amount, squares used
    visited = {n}

    while queue:
        remaining, used = queue.popleft()
        for s in squares:
            nxt = remaining - s
            if nxt == 0:
                return used + 1             # first arrival at 0 is optimal
            if nxt > 0 and nxt not in visited:
                visited.add(nxt)            # claim on discovery, not on removal
                queue.append((nxt, used + 1))
    return n
Crucial Notevalues are added to visited when they are generated, not when they are later popped. Marking on removal lets a value with several predecessors in the same layer be queued several times over, each copy re-expanding the same subtree — the queue swells and the linear guarantee is lost. Claiming on discovery ensures each value enters the queue exactly once, which is what bounds the whole search at n × √n.

Since the squares list is sorted ascending, the loop tries the smallest subtraction first. That does not affect correctness — every square in the layer is tried before the layer ends — it just means the trace below explores in a predictable order.

Work: at most n values, each with at most √n successors, so O(n√n) time and O(n) memory.

Worked example:n = 12

Squares that fit: 1, 4, 9.

- Start. Queue holds (12, 0). Visited {12}.
- Layer 1. Pop (12, 0). Subtracting 1, 4 and 9 gives 11, 8 and 3, none of them 0, all unvisited, so all three are claimed at 1 square used. Queue: [(11,1), (8,1), (3,1)].
- Layer 2. Pop (11,1), producing 10, 7 and 2. Pop (8,1), producing 7 — already claimed by 11 a moment ago, so skipped — and 4. Pop (3,1), producing 2, already claimed. So layer 2 is 10, 7, 2 and 4, each at 2 squares used. Notice 7 and 2 were each generated twice and queued once. That is the visited set doing its job inside a single layer.
- Layer 3. Pop (10,2), giving 9, 6 and 1, none zero. Pop (7,2), giving 6 (claimed) and 3 (claimed). Pop (2,2), giving 1 (claimed). Pop (4,2): subtracting 1 gives 3, claimed — and subtracting 4 gives exactly 0. Return 2 + 1 = 3.

The route was 12 → 8 → 4 → 0, subtracting 4 each time, which is precisely 12 = 4 + 4 + 4. The greedy route that grabs 9 first reaches 3, then 2, then 1, then 0 — four terms — and BFS simply never gets there first, because it finishes all of layer 3 before considering anything at depth 4.

For n = 7 the same process runs: layer 1 gives 6 and 3, layer 2 gives 5, 2 and 2 again, layer 3 gives 4 and 1, and layer 4 reaches 0 from 4 by subtracting 4 or from 1 by subtracting 1. The answer is 4, matching 4 + 1 + 1 + 1.

The same graph, walked differently

Because the future depends only on the current value, this can equally be solved by filling a table from the bottom up. Let dp[x] be the answer for x. Then dp[0] is 0, and for every other x, dp[x] is one more than the best dp[x - s] over all squares s that fit.

python
def num_squares(n):
    dp = [0] + [float("inf")] * n
    for x in range(1, n + 1):
        s = 1
        while s * s <= x:
            dp[x] = min(dp[x], dp[x - s * s] + 1)
            s += 1
    return dp[n]

This is O(n√n) as well — the same bound, and no wonder: it visits the same nodes and the same edges. It is the same graph traversed in a different order, computing every value rather than stopping at the first arrival. BFS can quit the moment it reaches 0, so it often touches less; the table is simpler to write and answers every value from 1 to n as a by-product. Either is a fine answer, and being able to say they are the same graph is what an interviewer is listening for.

There is also a closed-form curiosity worth knowing. Lagrange's four-square theorem proves every positive integer is the sum of at most four squares, so the answer is always 1, 2, 3 or 4 — never more. That is why n = 7 needing 4 is the worst case rather than an outlier. Testing 1 is a perfect-square check, 4 happens exactly when n has the form 4^a(8b + 7), and 2 can be tested by trying every square below n and checking whether the difference is a square; anything left over is 3. That gives an O(√n) solution, but it rests on a number-theory result rather than on any search idea, so lead with the graph and mention this as an aside.

The takeaway

The nodes here were plain integers, the edges were arithmetic, and nothing was ever stored as a graph. That is the point. A graph is a set of states plus a rule for moving between them, and the states can be anything a problem describes — a cell, a word, a lock code, a remaining amount.

Train the reflex: when a problem asks for the fewest operations to get from one value to another, and every operation counts the same, define the state, define the moves, and run BFS. And when the plain recursion looks exponential, check whether different histories can arrive at the same state — if they can, that state deserves to be computed once, whether you enforce that with a visited set or a table.

Interactive Strategy Visualization
Phase 1 — Start
BFS Over State Space
1 Explored
1 in Queue
Exploration Queue12 (0)

Start: Find the minimum number of perfect squares that sum to 12.

STEP 1/26
Exponential Recurse Every Combination
O(N × √N) BFS Or Bottom-Up DP
O(√N) Lagrange Four-Square Test