Please upgrade to NeetCode Pro to view company tags.
Prerequisites
Before attempting this problem, you should be comfortable with:
Recursion - The foundation for building the brute-force and memoized solutions
Dynamic Programming - Understanding both top-down (memoization) and bottom-up approaches for optimization
Breadth-First Search (BFS) - Used to model the problem as a shortest path search
Math (Number Theory) - Understanding perfect squares and Lagrange's four square theorem for the optimal solution
1. Recursion
Intuition
We want to express n as a sum of the fewest perfect squares. At each step, we can subtract any perfect square that fits, then recursively solve for the remainder. By trying all possible perfect squares and taking the minimum, we find the optimal answer. This brute-force approach explores all combinations but results in repeated subproblems.
Algorithm
Define a recursive function that takes a target value.
Base case: if target is 0, return 0 (no squares needed).
Initialize the result to target (the worst case is using all 1s).
For each perfect square i*i that does not exceed target, recursively solve for (target - i*i) and update the result with 1 + the recursive result.
classSolution:defnumSquares(self, n:int)->int:defdfs(target):if target ==0:return0
res = target
for i inrange(1, target):if i * i > target:break
res =min(res,1+ dfs(target - i * i))return res
return dfs(n)
publicclassSolution{publicintnumSquares(int n){returndfs(n);}privateintdfs(int target){if(target ==0){return0;}int res = target;for(int i =1; i * i <= target; i++){
res =Math.min(res,1+dfs(target - i * i));}return res;}}
classSolution{public:intnumSquares(int n){returndfs(n);}private:intdfs(int target){if(target ==0){return0;}int res = target;for(int i =1; i * i <= target; i++){
res =min(res,1+dfs(target - i * i));}return res;}};
classSolution{/**
* @param {number} n
* @return {number}
*/numSquares(n){constdfs=(target)=>{if(target ===0)return0;let res = target;for(let i =1; i * i <= target; i++){
res = Math.min(res,1+dfs(target - i * i));}return res;};returndfs(n);}}
publicclassSolution{publicintNumSquares(int n){returnDfs(n);}privateintDfs(int target){if(target ==0)return0;int res = target;for(int i =1; i * i <= target; i++){
res = Math.Min(res,1+Dfs(target - i * i));}return res;}}
funcnumSquares(n int)int{var dfs func(target int)int
dfs =func(target int)int{if target ==0{return0}
res := target
for i :=1; i*i <= target; i++{
res =min(res,1+dfs(target-i*i))}return res
}returndfs(n)}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funnumSquares(n: Int): Int {fundfs(target: Int): Int {if(target ==0)return0var res = target
var i =1while(i * i <= target){
res =minOf(res,1+dfs(target - i * i))
i++}return res
}returndfs(n)}}
classSolution{funcnumSquares(_ n:Int)->Int{funcdfs(_ target:Int)->Int{if target ==0{return0}var res = target
var i =1while i * i <= target {
res =min(res,1+dfs(target - i * i))
i +=1}return res
}returndfs(n)}}
Time & Space Complexity
Time complexity: O(nn)
Space complexity: O(n) for recursion stack.
2. Dynamic Programming (Top-Down)
Intuition
The recursive solution recomputes the same subproblems many times. By caching results in a memoization table, we avoid redundant work. Each unique target value is solved once, and subsequent calls return the cached result. This transforms the exponential time complexity into polynomial.
Algorithm
Create a memoization dictionary to store results for each target value.
Define a recursive function. If target is 0, return 0. If target is in memo, return the cached value.
Initialize result to target.
For each perfect square i*i up to target, compute 1 + dfs(target - i*i) and track the minimum.
classSolution:defnumSquares(self, n:int)->int:
memo ={}defdfs(target):if target ==0:return0if target in memo:return memo[target]
res = target
for i inrange(1, target +1):if i * i > target:break
res =min(res,1+ dfs(target - i * i))
memo[target]= res
return res
return dfs(n)
publicclassSolution{Map<Integer,Integer> memo =newHashMap<>();privateintdfs(int target){if(target ==0)return0;if(memo.containsKey(target))return memo.get(target);int res = target;for(int i =1; i * i <= target; i++){
res =Math.min(res,1+dfs(target - i * i));}
memo.put(target, res);return res;}publicintnumSquares(int n){returndfs(n);}}
classSolution{public:
unordered_map<int,int> memo;intdfs(int target){if(target ==0)return0;if(memo.count(target))return memo[target];int res = target;for(int i =1; i * i <= target; i++){
res =min(res,1+dfs(target - i * i));}return memo[target]= res;}intnumSquares(int n){returndfs(n);}};
classSolution{/**
* @param {number} n
* @return {number}
*/numSquares(n){const memo =newMap();constdfs=(target)=>{if(target ===0)return0;if(memo.has(target)){return memo.get(target);}let res = target;for(let i =1; i * i <= target; i++){
res = Math.min(res,1+dfs(target - i * i));}
memo.set(target, res);return res;};returndfs(n);}}
publicclassSolution{privateDictionary<int,int> memo =newDictionary<int,int>();publicintNumSquares(int n){returnDfs(n);}privateintDfs(int target){if(target ==0)return0;if(memo.ContainsKey(target))return memo[target];int res = target;for(int i =1; i * i <= target; i++){
res = Math.Min(res,1+Dfs(target - i * i));}
memo[target]= res;return res;}}
funcnumSquares(n int)int{
memo :=make(map[int]int)var dfs func(target int)int
dfs =func(target int)int{if target ==0{return0}if val, ok := memo[target]; ok {return val
}
res := target
for i :=1; i*i <= target; i++{
res =min(res,1+dfs(target-i*i))}
memo[target]= res
return res
}returndfs(n)}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funnumSquares(n: Int): Int {val memo = mutableMapOf<Int, Int>()fundfs(target: Int): Int {if(target ==0)return0if(memo.containsKey(target))return memo[target]!!var res = target
var i =1while(i * i <= target){
res =minOf(res,1+dfs(target - i * i))
i++}
memo[target]= res
return res
}returndfs(n)}}
classSolution{funcnumSquares(_ n:Int)->Int{var memo =[Int:Int]()funcdfs(_ target:Int)->Int{if target ==0{return0}iflet cached = memo[target]{return cached
}var res = target
var i =1while i * i <= target {
res =min(res,1+dfs(target - i * i))
i +=1}
memo[target]= res
return res
}returndfs(n)}}
Time & Space Complexity
Time complexity: O(n∗n)
Space complexity: O(n)
3. Dynamic Programming (Bottom-Up)
Intuition
Instead of solving top-down with recursion, we can build the solution bottom-up. We compute the minimum number of squares for every value from 1 to n, using previously computed results. For each target, we try subtracting every perfect square and take the minimum result plus one.
Algorithm
Create a dp array of size n+1, initialized to n (worst case of all 1s). Set dp[0] = 0.
For each target from 1 to n, iterate through all perfect squares s*s that do not exceed target.
classSolution:defnumSquares(self, n:int)->int:
dp =[n]*(n +1)
dp[0]=0for target inrange(1, n +1):for s inrange(1, target +1):
square = s * s
if target - square <0:break
dp[target]=min(dp[target],1+ dp[target - square])return dp[n]
publicclassSolution{publicintnumSquares(int n){int[] dp =newint[n +1];Arrays.fill(dp, n);
dp[0]=0;for(int target =1; target <= n; target++){for(int s =1; s * s <= target; s++){
dp[target]=Math.min(dp[target],1+ dp[target - s * s]);}}return dp[n];}}
classSolution{public:intnumSquares(int n){
vector<int>dp(n +1, n);
dp[0]=0;for(int target =1; target <= n; target++){for(int s =1; s * s <= target; s++){
dp[target]=min(dp[target],1+ dp[target - s * s]);}}return dp[n];}};
classSolution{/**
* @param {number} n
* @return {number}
*/numSquares(n){const dp =Array(n +1).fill(n);
dp[0]=0;for(let target =1; target <= n; target++){for(let s =1; s * s <= target; s++){
dp[target]= Math.min(dp[target],1+ dp[target - s * s]);}}return dp[n];}}
publicclassSolution{publicintNumSquares(int n){int[] dp =newint[n +1];
Array.Fill(dp, n);
dp[0]=0;for(int target =1; target <= n; target++){for(int s =1; s * s <= target; s++){int square = s * s;
dp[target]= Math.Min(dp[target],1+ dp[target - square]);}}return dp[n];}}
funcnumSquares(n int)int{
dp :=make([]int, n+1)for i :=range dp {
dp[i]= n
}
dp[0]=0for target :=1; target <= n; target++{for s :=1; s*s <= target; s++{
dp[target]=min(dp[target],1+dp[target-s*s])}}return dp[n]}funcmin(a, b int)int{if a < b {return a
}return b
}
class Solution {funnumSquares(n: Int): Int {val dp =IntArray(n +1){ n }
dp[0]=0for(target in1..n){var s =1while(s * s <= target){
dp[target]=minOf(dp[target],1+ dp[target - s * s])
s++}}return dp[n]}}
classSolution{funcnumSquares(_ n:Int)->Int{var dp =[Int](repeating: n, count: n +1)
dp[0]=0for target in1...n {var s =1while s * s <= target {
dp[target]=min(dp[target],1+ dp[target - s * s])
s +=1}}return dp[n]}}
Time & Space Complexity
Time complexity: O(n∗n)
Space complexity: O(n)
4. Breadth First Search
Intuition
We can view this as a shortest path problem. Starting from 0, each step adds a perfect square. BFS explores all sums reachable with 1 square, then 2 squares, and so on. The first time we reach n, we have found the minimum number of squares. Using a set to track visited values prevents processing the same sum multiple times.
Algorithm
Initialize a queue with 0 and a set to track seen values.
Process the queue level by level, incrementing the count at each level.
For each current sum, try adding every perfect square s*s such that current + s*s <= n.
If current + s*s equals n, return the current count.
If the new sum has not been seen, add it to the set and enqueue it.
classSolution:defnumSquares(self, n:int)->int:
q = deque()
seen =set()
res =0
q.append(0)while q:
res +=1for _ inrange(len(q)):
cur = q.popleft()
s =1while s * s + cur <= n:
nxt = cur + s * s
if nxt == n:return res
if nxt notin seen:
seen.add(nxt)
q.append(nxt)
s +=1return res
publicclassSolution{publicintnumSquares(int n){Queue<Integer> q =newLinkedList<>();Set<Integer> seen =newHashSet<>();int res =0;
q.offer(0);while(!q.isEmpty()){
res++;for(int i = q.size(); i >0; i--){int cur = q.poll();for(int s =1; s * s + cur <= n; s++){int next = cur + s * s;if(next == n)return res;if(!seen.contains(next)){
q.offer(next);
seen.add(next);}}}}return res;}}
classSolution{public:intnumSquares(int n){
queue<int> q;
unordered_set<int> seen;int res =0;
q.push(0);while(!q.empty()){
res++;for(int i = q.size(); i >0; i--){int cur = q.front(); q.pop();for(int s =1; s * s + cur <= n; s++){int next = cur + s * s;if(next == n)return res;if(seen.find(next)== seen.end()){
q.push(next);
seen.insert(next);}}}}return res;}};
classSolution{/**
* @param {number} n
* @return {number}
*/numSquares(n){const q =newQueue();const seen =newSet();let res =0;
q.push(0);while(!q.isEmpty()){
res++;for(let i = q.size(); i >0; i--){const cur = q.pop();for(let s =1; s * s + cur <= n; s++){const next = cur + s * s;if(next === n)return res;if(!seen.has(next)){
q.push(next);
seen.add(next);}}}}return res;}}
publicclassSolution{publicintNumSquares(int n){Queue<int> q =newQueue<int>();HashSet<int> seen =newHashSet<int>();int res =0;
q.Enqueue(0);while(q.Count >0){
res++;int size = q.Count;for(int i =0; i < size; i++){int cur = q.Dequeue();int s =1;while(s * s + cur <= n){int nxt = cur + s * s;if(nxt == n){return res;}if(!seen.Contains(nxt)){
seen.Add(nxt);
q.Enqueue(nxt);}
s++;}}}return res;}}
funcnumSquares(n int)int{
queue :=[]int{0}
seen :=make(map[int]bool)
res :=0forlen(queue)>0{
res++
size :=len(queue)for i :=0; i < size; i++{
cur := queue[0]
queue = queue[1:]for s :=1; s*s+cur <= n; s++{
next := cur + s*s
if next == n {return res
}if!seen[next]{
seen[next]=true
queue =append(queue, next)}}}}return res
}
class Solution {funnumSquares(n: Int): Int {val queue = ArrayDeque<Int>()val seen = mutableSetOf<Int>()var res =0
queue.add(0)while(queue.isNotEmpty()){
res++val size = queue.size
repeat(size){val cur = queue.removeFirst()var s =1while(s * s + cur <= n){val next = cur + s * s
if(next == n)return res
if(next !in seen){
seen.add(next)
queue.add(next)}
s++}}}return res
}}
classSolution{funcnumSquares(_ n:Int)->Int{var queue =[0]var seen =Set<Int>()var res =0while!queue.isEmpty {
res +=1let size = queue.count
for_in0..<size {let cur = queue.removeFirst()var s =1while s * s + cur <= n {let next = cur + s * s
if next == n {return res
}if!seen.contains(next){
seen.insert(next)
queue.append(next)}
s +=1}}}return res
}}
Time & Space Complexity
Time complexity: O(n∗n)
Space complexity: O(n)
5. Math
Intuition
Lagrange's four square theorem states that every positive integer can be expressed as the sum of at most four perfect squares. Using additional number theory, we can determine the exact answer in constant time. If n is a perfect square, the answer is 1. If n can be written as the sum of two squares, the answer is 2. If n is of the form 4^k(8m+7), the answer is 4. Otherwise, the answer is 3.
Algorithm
Remove all factors of 4 from n (divide by 4 while divisible).
If the reduced n is congruent to 7 mod 8, return 4.
Check if the original n is a perfect square. If so, return 1.
Check if n can be expressed as the sum of two squares by testing all possible first squares. If so, return 2.
classSolution:defnumSquares(self, n:int)->int:while n %4==0:
n //=4if n %8==7:return4defisSquareNum(num):
s =int(math.sqrt(num))return s * s == num
if isSquareNum(n):return1
i =1while i * i <= n:if isSquareNum(n - i * i):return2
i +=1return3
publicclassSolution{publicintnumSquares(int n){while(n %4==0){
n /=4;}if(n %8==7){return4;}if(isSquareNum(n)){return1;}for(int i =1; i * i <= n; i++){if(isSquareNum(n - i * i)){return2;}}return3;}privatebooleanisSquareNum(int num){int s =(int)Math.sqrt(num);return s * s == num;}}
classSolution{public:intnumSquares(int n){while(n %4==0){
n /=4;}if(n %8==7){return4;}if(isSquareNum(n)){return1;}for(int i =1; i * i <= n; i++){if(isSquareNum(n - i * i)){return2;}}return3;}private:boolisSquareNum(int num){int s =(int)sqrt(num);return s * s == num;}};
classSolution{/**
* @param {number} n
* @return {number}
*/numSquares(n){while(n %4===0){
n = Math.floor(n /4);}if(n %8===7){return4;}constisSquareNum=(num)=>{const s = Math.floor(Math.sqrt(num));return s * s === num;};if(isSquareNum(n)){return1;}for(let i =1; i * i <= n; i++){if(isSquareNum(n - i * i)){return2;}}return3;}}
publicclassSolution{publicintNumSquares(int n){while(n %4==0){
n /=4;}if(n %8==7){return4;}boolIsSquareNum(int num){int s =(int)Math.Sqrt(num);return s * s == num;}if(IsSquareNum(n)){return1;}for(int i =1; i * i <= n; i++){if(IsSquareNum(n - i * i)){return2;}}return3;}}
funcnumSquares(n int)int{for n%4==0{
n /=4}if n%8==7{return4}
isSquareNum :=func(num int)bool{
s :=int(math.Sqrt(float64(num)))return s*s == num
}ifisSquareNum(n){return1}for i :=1; i*i <= n; i++{ifisSquareNum(n - i*i){return2}}return3}
class Solution {funnumSquares(n: Int): Int {var num = n
while(num %4==0){
num /=4}if(num %8==7){return4}funisSquareNum(x: Int): Boolean {val s = kotlin.math.sqrt(x.toDouble()).toInt()return s * s == x
}if(isSquareNum(num)){return1}var i =1while(i * i <= num){if(isSquareNum(num - i * i)){return2}
i++}return3}}
classSolution{funcnumSquares(_ n:Int)->Int{var num = n
while num %4==0{
num /=4}if num %8==7{return4}funcisSquareNum(_ x:Int)->Bool{let s =Int(sqrt(Double(x)))return s * s == x
}ifisSquareNum(num){return1}var i =1while i * i <= num {ifisSquareNum(num - i * i){return2}
i +=1}return3}}
Time & Space Complexity
Time complexity: O(n)
Space complexity: O(1)
Common Pitfalls
Not Recognizing Overlapping Subproblems
A common mistake is implementing plain recursion without memoization. The recursive solution recomputes the same subproblems many times (e.g., dfs(5) might be called from multiple paths). Without caching results, the solution becomes exponentially slow and will time out on larger inputs.
Incorrect Base Case or Initialization
Forgetting to handle the base case dp[0] = 0 or initializing the DP array incorrectly leads to wrong answers. The value dp[0] = 0 is crucial because it represents that zero squares are needed to sum to zero. Similarly, initializing other values to n (worst case of all 1s) ensures the minimum is correctly computed.
Iterating Over Non-Perfect-Squares
Some implementations mistakenly iterate through all numbers from 1 to target instead of only perfect squares. This wastes computation and can lead to incorrect state transitions. Always ensure the inner loop only considers values i where i * i <= target.