算法279. 完全平方数
题目
给你一个整数 n ,返回 和为 n 的完全平方数的最少数量 。
完全平方数 是一个整数,其值等于另一个整数的平方;换句话说,其值等于一个整数自乘的积。例如,1、4、9 和 16 都是完全平方数,而 3 和 11 不是。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
提示:
1 <= n <= 104
题解
# 写在外面,多个测试数据之间可以共享,减少计算量
@cache # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i: int, j: int) -> int:if i == 0:return inf if j else 0if j < i * i:return dfs(i - 1, j) # 只能不选return min(dfs(i - 1, j), dfs(i, j - i * i) + 1) # 不选 vs 选class Solution:def numSquares(self, n: int) -> int:return dfs(isqrt(n), n)
代替@cache的题解
# 手动创建缓存字典
memo = {}
def dfs(i: int, j: int) -> int:# 1. 先查缓存if (i, j) in memo:return memo[(i, j)]# 2. 边界条件if i == 0:res = float('inf') if j else 0elif j < i * i:res = dfs(i - 1, j)else:res = min(dfs(i - 1, j), dfs(i, j - i * i) + 1)# 3. 存入缓存memo[(i, j)] = resreturn resclass Solution:def numSquares(self, n: int) -> int:return dfs(int(n**0.5), n)
解题思路
🎯 问题到底是什么?
题目:给一个正整数 n
,问最少用多少个完全平方数(1, 4, 9, 16, 25…)加起来等于 n
?
比如:
n = 12
可能的组合:- 4 + 4 + 4 → 用了 3 个
- 9 + 1 + 1 + 1 → 用了 4 个
- 1+1+…+1(12个1)→ 12 个
✅ 最少是 3 个
我们的目标:找到这个“最少个数”。
🤔 人类怎么思考这个问题?
假设 n = 12
,你会怎么想?
- 先看最大的平方数 ≤12 是多少?→ 9(因为 16>12)
- 然后考虑两种可能:
- 不用 9:那就在更小的平方数(1, 4)里凑 12
- 用 9:那剩下
12 - 9 = 3
,再用平方数凑 3(注意:还能继续用 9 吗?不能,因为 9>3,但以后如果剩下够大,是可以重复用的!)
🔁 关键:平方数可以重复用(比如 4+4+4),所以这是一个“可以回头再选”的问题。
🧠 代码的核心思想(用人话)
定义一个函数:
dfs(最大可用的平方根, 要凑的目标数)
→ 返回最少需要几个平方数
比如:
dfs(3, 12)
表示:“用 1², 2², 3²(即 1,4,9)来凑 12,最少要几个?”
那怎么算 dfs(3, 12)
?
- 选项1:不用 3²(即不用9)
→ 问题变成:用 1,4 凑 12 →dfs(2, 12)
- 选项2:用一个 3²(即用一个9)
→ 剩下12-9=3
,还要凑 3,而且还能继续用 9(虽然现在用不上,但函数设计要通用)→dfs(3, 3) + 1
(+1 是因为用了一个9)
然后选这两个选项中更小的那个:
min( dfs(2,12), dfs(3,3)+1 )
🔁 递归是怎么工作的?(以 n=12
为例)
我们画个简化版的递归树:
dfs(3,12)
├─ 不用9 → dfs(2,12)
│ ├─ 不用4 → dfs(1,12) → 只能用1 → 需要12个
│ └─ 用4 → dfs(2,8)+1
│ ├─ 用4 → dfs(2,4)+1
│ │ └─ 用4 → dfs(2,0)+1 = 0+1=1 → 所以 dfs(2,4)=1
│ └─ 所以 dfs(2,8) = min(..., 1+1=2) → 最终 dfs(2,12)=3
└─ 用9 → dfs(3,3)+1└─ 3<9 → 只能用更小的 → dfs(2,3)└─ 3<4 → 只能用1 → dfs(1,3)=3→ 所以 dfs(3,3)+1 = 3+1 = 4
最终:min(3, 4) = 3
✅
❓ 代码里的 i == 0
是什么意思?
i
是“最大可用的平方根”,比如i=3
对应最大用 9。- 当
i=0
,意味着“最大只能用 0²=0”。- 如果目标
j=0
:什么都不用,0 个 → 返回 0 - 如果目标
j>0
:只能用 0 凑正数,不可能 → 返回一个很大的数(inf
),这样在min()
里会被忽略
- 如果目标
❓ 为什么用 @cache
?
因为递归过程中会重复计算相同的问题。
比如 dfs(2,4)
可能在多个分支中被计算多次。
@cache
会记住 (i, j)
的结果,下次直接返回,避免重复劳动。
❓ 为什么初始调用是 dfs(isqrt(n), n)
?
isqrt(n)
是n
的整数平方根。- 比如
n=12
→isqrt(12)=3
,因为3²=9 ≤12
,而4²=16>12
- 比如
- 所以最大的有用平方数就是
3²=9
,没必要考虑更大的(比如16,根本用不上)
🧩 一句话总结代码逻辑
从最大的平方数开始,对每个数决定“用”还是“不用”,递归求解剩下的目标,最后选个数最少的方案。用缓存避免重复计算。
💡 如果还是抽象,记住这个模板
这类“最少多少个数凑成目标”的问题,通常可以这样想:
最少个数(目标) = min(最少个数(目标 - 选一个a) + 1,最少个数(目标 - 选一个b) + 1,...
)
而这段代码只是用“从大到小枚举平方数”的方式实现了这个思想。
总结
这是一个“完全背包”的问题,关于解答此类问题的解法“完全背包”和“0-1背包”,见另一篇文章。
【动态规划】题目中的「0-1 背包」和「完全背包」的问题