279. Perfect Squares
Problem Description
Given an integer n
, you need to find the minimum number of perfect square numbers that add up to exactly n
.
A perfect square is a number that can be expressed as the product of an integer with itself. For example:
1 = 1 × 1
is a perfect square4 = 2 × 2
is a perfect square9 = 3 × 3
is a perfect square16 = 4 × 4
is a perfect square- Numbers like
3
and11
are not perfect squares
The task is to determine the least number of these perfect squares needed to sum to the given number n
.
For example:
- If
n = 12
, one possible way is12 = 4 + 4 + 4
(three perfect squares) - Another way is
12 = 4 + 8
, but8
is not a perfect square - The optimal answer for
n = 12
is3
The solution uses dynamic programming with a 2D table f[i][j]
where:
i
represents considering perfect squares from1²
toi²
j
represents the target sum we're trying to achievef[i][j]
stores the minimum number of perfect squares needed to sum toj
using squares up toi²
The algorithm iterates through all possible perfect squares up to √n
and for each target value, decides whether to include the current perfect square or not, choosing the option that gives the minimum count.
Flowchart Walkthrough
First, let's pin down the algorithm using the Flowchart. Here's a step-by-step walkthrough:
Is it a graph?
- Yes: We can model this problem as a graph where each number from 0 to n is a node, and there's an edge from number
a
to numberb
ifb = a + (perfect square)
. We're looking for the shortest path from 0 to n.
Is it a tree?
- No: The graph is not a tree since multiple paths can lead to the same number (e.g., 12 can be reached as 0→4→8→12 or 0→9→12).
Is the problem related to directed acyclic graphs?
- No: While the graph is directed (we only add perfect squares, never subtract), the problem is fundamentally about finding the shortest path.
Is the problem related to shortest paths?
- Yes: We need to find the minimum number of perfect squares (minimum number of edges/steps) to reach n from 0.
Is the graph Weighted?
- No: Each edge has the same weight of 1 (adding one perfect square counts as one step).
BFS
- Yes: Since we have an unweighted graph and need the shortest path, BFS is the appropriate algorithm.
Conclusion: The flowchart suggests using BFS (Breadth-First Search) for finding the minimum number of perfect squares that sum to n. In the BFS approach, we would start from n and subtract all possible perfect squares, treating each valid subtraction as moving to a new node, until we reach 0. The level at which we reach 0 gives us the minimum number of perfect squares needed.
Intuition
The key insight is recognizing that this problem can be viewed as finding the shortest path in an implicit graph. Imagine starting at number n
and trying to reach 0
by subtracting perfect squares at each step. Each subtraction represents moving to a new node in our graph.
For example, if n = 12
:
- From 12, we can subtract 1 (1²), 4 (2²), or 9 (3²) to reach 11, 8, or 3
- From 8, we can subtract 1 or 4 to reach 7 or 4
- From 4, we can subtract 1 or 4 to reach 3 or 0
- When we reach 0, we've found a valid decomposition
Since each step (subtracting a perfect square) has equal weight, we want the path with the fewest steps. This naturally leads to BFS, which explores all possibilities level by level and guarantees finding the shortest path first.
However, the given solution uses dynamic programming instead of BFS. The DP approach builds up the answer systematically by considering: "If I can use perfect squares up to i²
, what's the minimum number needed to sum to j
?"
The recurrence relation is:
- Either don't use the current perfect square
i²
:f[i][j] = f[i-1][j]
- Or use it:
f[i][j] = f[i][j - i*i] + 1
(ifj >= i*i
)
We take the minimum of these two options. The beauty of this approach is that it considers all possible combinations systematically, similar to how the unbounded knapsack problem allows unlimited use of each item. Here, we can use each perfect square as many times as needed to reach our target sum.
Solution Approach
The solution implements a dynamic programming approach using a 2D table to solve this unbounded knapsack variant.
Data Structure Setup:
- Calculate
m = int(sqrt(n))
to find the maximum perfect square we need to consider - Create a 2D DP table
f
with dimensions(m+1) × (n+1)
initialized with infinity f[i][j]
represents the minimum number of perfect squares needed to sum toj
using squares from1²
toi²
- Set
f[0][0] = 0
as base case (0 squares needed to sum to 0)
Dynamic Programming Iteration: The algorithm iterates through each perfect square and each target sum:
for i in range(1, m + 1): # Consider squares from 1² to i²
for j in range(n + 1): # For each target sum from 0 to n
f[i][j] = f[i - 1][j] # Option 1: Don't use i²
if j >= i * i: # If we can use i²
f[i][j] = min(f[i][j], f[i][j - i * i] + 1) # Option 2: Use i²
State Transition Logic:
For each state f[i][j]
, we have two choices:
- Don't use the current perfect square
i²
: Copy the result fromf[i-1][j]
(minimum squares needed using only1²
to(i-1)²
) - Use the current perfect square
i²
: Take the result fromf[i][j - i*i]
and add 1 (we can reusei²
multiple times, hence we look atf[i][...]
notf[i-1][...]
)
The key insight is that we check f[i][j - i*i]
instead of f[i-1][j - i*i]
, allowing unlimited use of each perfect square (unbounded knapsack property).
Final Result:
Return f[m][n]
, which contains the minimum number of perfect squares needed to sum to n
using all available perfect squares from 1²
to m²
.
Ready to land your dream job?
Unlock your dream job with a 3-minute evaluator for a personalized learning plan!
Start EvaluatorExample Walkthrough
Let's walk through the solution with n = 12
.
Step 1: Setup
- Calculate
m = int(sqrt(12)) = 3
, so we'll consider perfect squares: 1², 2², 3² (which are 1, 4, 9) - Create DP table
f[4][13]
(dimensions are(m+1) × (n+1)
) - Initialize all values to infinity, except
f[0][0] = 0
Step 2: Build DP Table
Let me show the key iterations:
When i = 1 (considering square 1² = 1):
- For j = 0:
f[1][0] = f[0][0] = 0
(no squares needed for sum 0) - For j = 1:
- Don't use 1²:
f[1][1] = f[0][1] = ∞
- Use 1²:
f[1][1] = f[1][0] + 1 = 0 + 1 = 1
- Take minimum:
f[1][1] = 1
- Don't use 1²:
- For j = 2:
f[1][2] = min(∞, f[1][1] + 1) = 2
(need two 1's) - ...continuing this pattern...
- For j = 12:
f[1][12] = 12
(need twelve 1's)
When i = 2 (considering squares 1² and 2² = 4):
- For j = 0 to 3: Same as row i = 1 (can't use 4 for these)
- For j = 4:
- Don't use 4:
f[2][4] = f[1][4] = 4
(four 1's) - Use 4:
f[2][4] = f[2][0] + 1 = 0 + 1 = 1
(one 4) - Take minimum:
f[2][4] = 1
- Don't use 4:
- For j = 8:
- Don't use 4:
f[2][8] = f[1][8] = 8
- Use 4:
f[2][8] = f[2][4] + 1 = 1 + 1 = 2
(two 4's) - Take minimum:
f[2][8] = 2
- Don't use 4:
- For j = 12:
- Don't use 4:
f[2][12] = f[1][12] = 12
- Use 4:
f[2][12] = f[2][8] + 1 = 2 + 1 = 3
(three 4's) - Take minimum:
f[2][12] = 3
- Don't use 4:
When i = 3 (considering squares 1², 2², and 3² = 9):
- For j = 0 to 8: Same as row i = 2 (can't use 9 for these)
- For j = 9:
- Don't use 9:
f[3][9] = f[2][9] = 3
(e.g., 4+4+1) - Use 9:
f[3][9] = f[3][0] + 1 = 0 + 1 = 1
(one 9) - Take minimum:
f[3][9] = 1
- Don't use 9:
- For j = 12:
- Don't use 9:
f[3][12] = f[2][12] = 3
(three 4's) - Use 9:
f[3][12] = f[3][3] + 1 = 3 + 1 = 4
(9+1+1+1) - Take minimum:
f[3][12] = 3
- Don't use 9:
Step 3: Final Answer
Return f[3][12] = 3
The minimum number of perfect squares that sum to 12 is 3, achieved by using 4 + 4 + 4.
Solution Implementation
1class Solution:
2 def numSquares(self, n: int) -> int:
3 """
4 Find the least number of perfect square numbers that sum to n.
5 Uses dynamic programming with 2D DP table.
6
7 Args:
8 n: Target integer to represent as sum of perfect squares
9
10 Returns:
11 Minimum number of perfect squares that sum to n
12 """
13 from math import sqrt, inf
14
15 # Calculate the maximum perfect square we need to consider
16 max_square_root = int(sqrt(n))
17
18 # Initialize DP table
19 # dp[i][j] = minimum number of perfect squares needed to sum to j
20 # using only the first i perfect squares (1², 2², ..., i²)
21 dp = [[inf] * (n + 1) for _ in range(max_square_root + 1)]
22
23 # Base case: 0 can be represented with 0 perfect squares
24 dp[0][0] = 0
25
26 # Fill the DP table
27 for i in range(1, max_square_root + 1):
28 for j in range(n + 1):
29 # Option 1: Don't use i² in the sum
30 dp[i][j] = dp[i - 1][j]
31
32 # Option 2: Use i² if possible
33 if j >= i * i:
34 # Take minimum of not using i² vs using it
35 dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1)
36
37 # Return the minimum number of perfect squares needed to sum to n
38 return dp[max_square_root][n]
39
1class Solution {
2 public int numSquares(int n) {
3 // Calculate the maximum perfect square that could be used (sqrt of n)
4 int maxSquareRoot = (int) Math.sqrt(n);
5
6 // Create DP table: dp[i][j] represents minimum number of perfect squares
7 // needed to sum to j using perfect squares from 1^2 to i^2
8 int[][] dp = new int[maxSquareRoot + 1][n + 1];
9
10 // Initialize all values to a large number (infinity)
11 for (int[] row : dp) {
12 Arrays.fill(row, 1 << 30); // Using bit shift for large value (2^30)
13 }
14
15 // Base case: 0 squares needed to sum to 0
16 dp[0][0] = 0;
17
18 // Fill the DP table
19 for (int i = 1; i <= maxSquareRoot; i++) {
20 int currentSquare = i * i; // The current perfect square being considered
21
22 for (int j = 0; j <= n; j++) {
23 // Option 1: Don't use the current perfect square i^2
24 dp[i][j] = dp[i - 1][j];
25
26 // Option 2: Use the current perfect square i^2 (if possible)
27 if (j >= currentSquare) {
28 // Take minimum of not using current square vs using it
29 dp[i][j] = Math.min(dp[i][j], dp[i][j - currentSquare] + 1);
30 }
31 }
32 }
33
34 // Return the minimum number of perfect squares that sum to n
35 return dp[maxSquareRoot][n];
36 }
37}
38
1class Solution {
2public:
3 int numSquares(int n) {
4 // Calculate the maximum perfect square we need to consider
5 int maxSquare = sqrt(n);
6
7 // dp[i][j] represents the minimum number of perfect squares needed to sum to j
8 // using perfect squares from 1^2 to i^2
9 int dp[maxSquare + 1][n + 1];
10
11 // Initialize all values to a large number (infinity)
12 memset(dp, 0x3f, sizeof(dp));
13
14 // Base case: 0 perfect squares sum to 0
15 dp[0][0] = 0;
16
17 // Iterate through each perfect square from 1^2 to maxSquare^2
18 for (int i = 1; i <= maxSquare; ++i) {
19 // For each target sum from 0 to n
20 for (int j = 0; j <= n; ++j) {
21 // Option 1: Don't use the current perfect square i^2
22 dp[i][j] = dp[i - 1][j];
23
24 // Option 2: Use the current perfect square i^2 if possible
25 if (j >= i * i) {
26 // Take minimum between not using i^2 and using i^2
27 dp[i][j] = min(dp[i][j], dp[i][j - i * i] + 1);
28 }
29 }
30 }
31
32 // Return the minimum number of perfect squares that sum to n
33 return dp[maxSquare][n];
34 }
35};
36
1/**
2 * Finds the minimum number of perfect square numbers that sum to n
3 * Uses dynamic programming approach similar to unbounded knapsack problem
4 * @param n - The target number to decompose into perfect squares
5 * @returns The minimum number of perfect squares that sum to n
6 */
7function numSquares(n: number): number {
8 // Calculate the maximum possible square root we need to consider
9 const maxSquareRoot = Math.floor(Math.sqrt(n));
10
11 // Initialize DP table where dp[i][j] represents the minimum number of perfect squares
12 // needed to sum to j using perfect squares from 1^2 to i^2
13 // Initialize with a large value (infinity-like) to represent impossible states
14 const dp: number[][] = Array(maxSquareRoot + 1)
15 .fill(0)
16 .map(() => Array(n + 1).fill(1 << 30));
17
18 // Base case: 0 requires 0 perfect squares
19 dp[0][0] = 0;
20
21 // Iterate through each perfect square from 1^2 to maxSquareRoot^2
22 for (let i = 1; i <= maxSquareRoot; ++i) {
23 // For each target sum from 0 to n
24 for (let j = 0; j <= n; ++j) {
25 // Option 1: Don't use the current perfect square i^2
26 dp[i][j] = dp[i - 1][j];
27
28 // Option 2: Use the current perfect square i^2 if possible
29 if (j >= i * i) {
30 // Take the minimum between not using i^2 and using it
31 dp[i][j] = Math.min(dp[i][j], dp[i][j - i * i] + 1);
32 }
33 }
34 }
35
36 // Return the minimum number of perfect squares needed to sum to n
37 return dp[maxSquareRoot][n];
38}
39
Time and Space Complexity
Time Complexity: O(n * sqrt(n))
The algorithm uses dynamic programming with a 2D table. The outer loop runs from 1
to m
where m = int(sqrt(n))
, giving us O(sqrt(n))
iterations. The inner loop runs from 0
to n
, giving us O(n)
iterations. Since these loops are nested, the total time complexity is O(sqrt(n) * n)
= O(n * sqrt(n))
.
Space Complexity: O(n * sqrt(n))
The algorithm creates a 2D array f
with dimensions (m + 1) × (n + 1)
where m = int(sqrt(n))
. This results in a space requirement of O((sqrt(n) + 1) * (n + 1))
= O(n * sqrt(n))
for storing the DP table. No additional significant space is used beyond this table, so the overall space complexity remains O(n * sqrt(n))
.
Common Pitfalls
1. Memory Inefficiency with Large Values of n
The current solution uses O(m × n) space where m = √n, resulting in O(n√n) space complexity. For large values of n (e.g., n = 10,000), this creates a table with millions of entries, which is unnecessary since we only need the previous row to compute the current row.
Solution: Optimize to 1D DP array since we only need the current state:
def numSquares(self, n: int) -> int:
from math import sqrt, inf
# Use 1D DP array instead of 2D
dp = [inf] * (n + 1)
dp[0] = 0
max_square_root = int(sqrt(n))
for i in range(1, max_square_root + 1):
square = i * i
for j in range(square, n + 1):
dp[j] = min(dp[j], dp[j - square] + 1)
return dp[n]
2. Initialization Error with Base Cases
A common mistake is forgetting to properly initialize all base cases. The code only sets dp[0][0] = 0
, but doesn't handle dp[i][0]
for i > 0, relying on the iteration to propagate the value. If the loop logic changes, this could break.
Solution: Explicitly initialize all dp[i][0] = 0
values:
# Initialize all dp[i][0] = 0 (0 squares needed for sum 0)
for i in range(max_square_root + 1):
dp[i][0] = 0
3. Integer Overflow in Square Calculation
Computing i * i
repeatedly in the inner loop is inefficient and could theoretically cause issues with very large numbers (though Python handles big integers well).
Solution: Pre-compute the square value:
for i in range(1, max_square_root + 1):
square = i * i # Compute once per outer loop
for j in range(n + 1):
dp[i][j] = dp[i - 1][j]
if j >= square:
dp[i][j] = min(dp[i][j], dp[i][j - square] + 1)
4. Missing Edge Case: n = 0
While the code handles n = 0 correctly, it's worth noting that some implementations might not account for this edge case where the answer should be 0.
Solution: Add explicit edge case handling if needed:
if n == 0: return 0
5. Suboptimal Algorithm Choice
While the 2D DP approach works, this problem can be solved more efficiently using BFS or mathematical approaches (Lagrange's four-square theorem). The DP solution has O(n√n) time complexity, while BFS can potentially find the answer faster for certain inputs.
Alternative BFS Solution:
def numSquares(self, n: int) -> int:
from collections import deque
squares = []
i = 1
while i * i <= n:
squares.append(i * i)
i += 1
queue = deque([n])
level = 0
visited = {n}
while queue:
level += 1
for _ in range(len(queue)):
remainder = queue.popleft()
for square in squares:
if remainder == square:
return level
if remainder < square:
break
next_val = remainder - square
if next_val not in visited:
visited.add(next_val)
queue.append(next_val)
return level
A heap is a ...?
Recommended Readings
https assets algo monster cover_photos bfs svg Breadth First Search on Trees Hopefully by this time you've drunk enough DFS Kool Aid to understand its immense power and seen enough visualization to create a call stack in your mind Now let me introduce the companion spell Breadth First Search BFS
Math for Technical Interviews How much math do I need to know for technical interviews The short answer is about high school level math Computer science is often associated with math and some universities even place their computer science department under the math faculty However the reality is that you
What is Dynamic Programming Prerequisite DFS problems dfs_intro Backtracking problems backtracking Memoization problems memoization_intro Pruning problems backtracking_pruning Dynamic programming is an algorithmic optimization technique that breaks down a complicated problem into smaller overlapping sub problems in a recursive manner and uses solutions to the sub problems to construct a solution
Want a Structured Path to Master System Design Too? Don’t Miss This!