算法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 背包」和「完全背包」的问题

相关推荐
Blossom.1188 小时前
把AI“贴”进路灯柱:1KB决策树让老旧路灯自己报「灯头松动」
java·人工智能·python·深度学习·算法·决策树·机器学习
墨染点香10 小时前
LeetCode 刷题【144. 二叉树的前序遍历】
数据结构·算法·leetcode
cynicme15 小时前
力扣3318——计算子数组的 x-sum I(偷懒版)
java·算法·leetcode
im_AMBER18 小时前
算法笔记 09
c语言·数据结构·c++·笔记·学习·算法·排序算法
凯芸呢18 小时前
Java中的数组(续)
java·开发语言·数据结构·算法·青少年编程·排序算法·idea
寂静山林18 小时前
UVa 1030 Image Is Everything
算法
AI柠檬18 小时前
几种排序算法的实现和性能比较
数据结构·算法·c#·排序算法
weixin_4296302619 小时前
第6章 支持向量机
算法·机器学习·支持向量机
SweetCode19 小时前
C++ 实现大数加法
开发语言·c++·算法
王哈哈^_^19 小时前
【数据集】【YOLO】【目标检测】共享单车数据集,共享单车识别数据集 3596 张,YOLO自行车识别算法实战训推教程。
人工智能·算法·yolo·目标检测·计算机视觉·视觉检测·毕业设计