算法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

题解

python 复制代码
# 写在外面,多个测试数据之间可以共享,减少计算量
@cache  # 缓存装饰器,避免重复计算 dfs 的结果(记忆化)
def dfs(i: int, j: int) -> int:
    if i == 0:
        return inf if j else 0
    if 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的题解

python 复制代码
# 手动创建缓存字典
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 0
    elif 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)] = res
    return res

class 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,你会怎么想?

  1. 先看最大的平方数 ≤12 是多少?→ 9(因为 16>12)
  2. 然后考虑两种可能:
    • 不用 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)

然后选这两个选项中更小的那个

python 复制代码
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=12isqrt(12)=3,因为 3²=9 ≤12,而 4²=16>12
  • 所以最大的有用平方数就是 3²=9,没必要考虑更大的(比如16,根本用不上)

🧩 一句话总结代码逻辑

从最大的平方数开始,对每个数决定"用"还是"不用",递归求解剩下的目标,最后选个数最少的方案。用缓存避免重复计算。


💡 如果还是抽象,记住这个模板

这类"最少多少个数凑成目标"的问题,通常可以这样想:

text 复制代码
最少个数(目标) = min(
    最少个数(目标 - 选一个a) + 1,
    最少个数(目标 - 选一个b) + 1,
    ...
)

而这段代码只是用"从大到小枚举平方数"的方式实现了这个思想。


总结

这是一个"完全背包"的问题,关于解答此类问题的解法"完全背包"和"0-1背包",见另一篇文章。
【动态规划】题目中的「0-1 背包」和「完全背包」的问题

相关推荐
iナナ3 小时前
Java优选算法——位运算
java·数据结构·算法·leetcode
sali-tec3 小时前
C# 基于halcon的视觉工作流-章45-网格面划痕
开发语言·算法·计算机视觉·c#
通信小呆呆3 小时前
5G NR 信号检测:从 PSS 相关到 SSB 栅格恢复
算法·5g
Han.miracle4 小时前
数据结构二叉树——层序遍历&& 扩展二叉树的左视图
java·数据结构·算法·leetcode
筱砚.4 小时前
【数据结构——最小生成树与Kruskal】
数据结构·算法
噢,我明白了4 小时前
前端js 常见算法面试题目详解
前端·javascript·算法
怎么没有名字注册了啊5 小时前
查找成绩(数组实现)
c++·算法
沐怡旸5 小时前
【算法】725.分割链表--通俗讲解
算法·面试
L_09077 小时前
【Algorithm】Day-4
c++·算法·leetcode