279. 完全平方数
LeetCode 2791 · 中等 · 动态规划
题
给正整数 n,把它写成若干个完全平方数(1, 4, 9, 16, …)之和,求最少需要几个。
例:n = 12 → 3(4 + 4 + 4);n = 13 → 2(4 + 9)。
一句话
dp[i] = 1 + min(dp[i - j²]):枚举最后用的那个平方数 j²,剩下的 i - j² 已经算好了。
关键技巧
识别成完全背包。 物品是 \(1^2, 2^2, \dots, \lfloor\sqrt n\rfloor^2\),每种可用无限次,求凑满容量 \(n\) 的最少物品数——和 322. 零钱兑换 是同一道题换了皮。
- 状态:\(f(i)\) = 和为 \(i\) 最少需要几个平方数。
- 转移:\(f(i) = 1 + \min_{j^2 \le i} f(i - j^2)\)。
- 初值:\(f(0) = 0\)。任何 \(i\) 都能用全 1 凑出来,所以不会无解。
- 顺序:\(i\) 递增。
为什么贪心不行。 每次取不超过余量的最大平方数:12 → 9 + 1 + 1 + 1 要 4 个,而最优是 4 + 4 + 4。局部最大不保证全局最少,所以要 DP 枚举所有「最后一块」。
数学捷径(知道即可)。 四平方和定理:任何正整数至多 4 个平方数之和;且 \(n = 4^a(8b+7)\) 时恰好要 4 个。据此可以 \(O(\sqrt n)\) 判答案是 1、2、3、4 中的哪个。
解
class Solution:
def numSquares(self, n: int) -> int:
squares = [j * j for j in range(1, isqrt(n) + 1)]
dp = [0] + [n] * n # 全 1 凑法是上界
for i in range(1, n + 1):
for sq in squares:
if sq > i:
break
dp[i] = min(dp[i], dp[i - sq] + 1)
return dp[n]
时间 \(O(n\sqrt n)\),空间 \(O(n)\)。
延伸
- 同构:322. 零钱兑换,硬币面额任意、可能无解,要多一个「凑不出」的哨兵。
- BFS 视角:从
n出发,每步减一个平方数,求到 0 的最少步数——层序 BFS 同样正确,思路见 45. 跳跃游戏 II。 - Python 超时的坑:LeetCode 上 Python 跑 \(O(n\sqrt n)\) 偏慢,可以把
dp做成类级别缓存让多个用例共享。
自测
-
LeetCode 279,2026-09-29 读。 ↩