Problem of The Day: Perfect Squares
Problem Statement
TLE Approaches
Memoization - TLE
class Solution:
    def numSquares(self, n: int) -> int:
        nums = []
        for i in range(1, n + 1):
            square = i**2
            if square <= n:
                nums.append(square)
            else:
                break
        
        nums.sort(reverse=True)
        memo = defaultdict()
        
        def dfs(i, n):
            if n < 0:
                return float('inf')
            if n == 0:
                return 0
            if (i, n) in memo:
                return memo[(i, n)]
            res = float('inf')
            for j in range(i, len(nums)):
                res = min(res, dfs(j, n - nums[j]) + 1, dfs(j + 1, n) + 1)
            
            memo[(i, n)] = res
            return res
        return dfs(0, n)
Dynamic Programming - TLE
class Solution:
    def numSquares(self, n: int) -> int:
        dp = [n] * (n + 1)
        for i in range(n + 1):
            if i ** 2 > n:
                break
            dp[i**2] = 1
        
        for i in range(1, n + 1):
            for j in range(0, i//2 + 1):
                dp[i] = min(dp[i], dp[i - j] + dp[j])
        
        return dp[-1]
BFS Approach - Memory Limit Exceeded
class Solution:
    def numSquares(self, n: int) -> int:
        nums = []
        for i in range(n + 1):
            if i ** 2 > n:
                break
            nums.append(i**2)
        nums.sort(reverse=True)
        queue = deque()
        queue.append([n, 0])
        while queue:
            total, level = queue.popleft()
            if total < 0:
                continue
            if total == 0:
                return level
            
            for num in nums:
                queue.append([total - num, level + 1])
        
        return res
Improved BFS Approach - Accepted
Intuition
My initial thought is to use a breadth-first search (BFS) approach to explore all possible combinations of perfect square numbers that add up to the given integer n. By breaking down the problem into smaller subproblems, we can efficiently find the minimum number of squares required.
Approach
I start by creating a list of perfect square numbers up to n and use a queue to perform BFS. The queue contains pairs of the remaining total (total) and the current level of the BFS (level). I explore all possible combinations by subtracting each perfect square number from the total and enqueueing the updated values. The process continues until I find a combination that results in total being zero.
I keep track of the level during BFS, and when I find a combination that satisfies the condition (total - num == 0), I set the result (res) to the current level plus 1. I also break out of the loop to stop further exploration since we aim to find the minimum number of squares.
Complexity
- 
    Time complexity: O(n * sqrt(n)), where nis the given integer. The loop to find perfect squares takes O(sqrt(n)), and in the worst case, we may explore all combinations.
- 
    Space complexity: O(n) for the queue, as it can grow up to the size of n.
Code
class Solution:
    def numSquares(self, n: int) -> int:
        nums = []
        for i in range(n + 1):
            if i ** 2 > n:
                break
            nums.append(i**2)
        queue = deque()
        queue.append([n, 0])
        res = 0
        while queue:
            total, level = queue.popleft()
            for num in nums:
                if total - num == 0:
                    res = level + 1
                    break
                if total - num < 0:
                    continue
                queue.append([total - num, level + 1])
            if res > 0:
                break
        return res
