LeetCode 279. 完全平方数

题目描述

给定一个正整数 n,返回和为 n 的完全平方数的最少数量。

完全平方数是某个整数的平方,例如 1、4、9 和 16。

例如:

复制代码
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4。

原始思路:递归枚举

可以把每个完全平方数看作一次选择。选择一个平方数后,递归计算剩余数还需要多少个平方数。核心是对所有候选方案取最小值,而不是让后一次赋值覆盖前一次结果。

伪代码:

复制代码
函数 dfs(x):
    如果 x <= 0:
        返回 0
    best <- 正无穷
    对每个不超过 x 的完全平方数 square:
        best <- min(best, 1 + dfs(x - square))
    返回 best

对应 Java 代码:

复制代码
class Solution {
    public int dfs(int n) {
        if (n <= 0) return 0;
        int best = Integer.MAX_VALUE;
        for (int j = 1; j <= n; j++) {
            if (Math.sqrt(j) * Math.sqrt(j) == j) {
                best = Math.min(best, dfs(n - j) + 1);
            }
        }
        return best;
    }

    public int numSquares(int n) {
        return dfs(n);
    }
}

这种递归会重复计算相同的剩余值,时间复杂度较高,因此需要缓存状态。

状态定义与转移

定义 dfs(i, j) 为使用前 i 个完全平方数凑出 j 所需的最少数量,其中第 i 个候选平方数为 i*i。

对 i*i 有两种选择:不使用它,转移到 dfs(i - 1, j);使用它,转移到 dfs(i, j - i*i) + 1。使用后仍保留 i,因为完全平方数可以重复使用,例如 12 = 4 + 4 + 4。

复制代码
dfs(i, j) = min(dfs(i - 1, j), dfs(i, j - i*i) + 1)

当 i = 0 时,j = 0 返回 0,j > 0 表示不可达。不可达状态使用 Integer.MAX_VALUE / 2 表示,避免后续加 1 时整数溢出。

记忆化搜索

伪代码:

复制代码
函数 dfs(i, j):
    如果 i == 0:
        如果 j == 0,返回 0
        否则返回 INF
    如果 i*i > j:
        返回 dfs(i - 1, j)
    如果 memo[i][j] 已计算:
        返回 memo[i][j]
    memo[i][j] <- min(dfs(i - 1, j), dfs(i, j - i*i) + 1)
    返回 memo[i][j]

对应 Java 代码:

复制代码
class Solution {
    int[][] memo = new int[101][10001];

    public int dfs(int i, int j) {
        if (i == 0) {
            return j == 0 ? 0 : Integer.MAX_VALUE / 2;
        }
        if (i * i > j) {
            return memo[i][j] = dfs(i - 1, j);
        }
        if (memo[i][j] != 0) {
            return memo[i][j];
        }
        return memo[i][j] = Math.min(
            dfs(i - 1, j),
            dfs(i, j - i * i) + 1
        );
    }

    public int numSquares(int n) {
        return dfs((int) Math.sqrt(n), n);
    }
}

易错点

1. 直接覆盖最优结果

尝试多个平方数时必须持续取 Math.min,不能只把当前候选直接赋给结果。

2. 共享成员变量 best

每个状态都应有自己的最优值。把 best 作为成员变量会让子问题覆盖父问题的计算,应使用局部变量或直接写入当前 memo[i][j]。

3. 错误减少 i

使用 i*i 后必须递归到 dfs(i, j - i*i),否则会禁止重复使用当前平方数。

4. 无穷大加一溢出

不能让 Integer.MAX_VALUE 参与 + 1,应使用 Integer.MAX_VALUE / 2 或先判断不可达状态。

建议测试用例

复制代码
n = 1  -> 1
n = 4  -> 1
n = 12 -> 3
n = 13 -> 2

复杂度分析

  • 时间复杂度:O(n√n),二维状态数量为 O(n√n)。
  • 空间复杂度:O(n√n),主要来自二维 memo 数组。

复盘

本题的关键是明确两个状态维度、保留当前平方数以支持重复使用,并在所有方案中取最小值。最终递推式为:

复制代码
dfs(i, j) = min(dfs(i - 1, j), dfs(i, j - i*i) + 1)
相关推荐
凤凰院凶涛QAQ24 分钟前
《Java版数据结构 & 集合类剖析》Map & Set & 反射:“从哈希碰撞到红黑树转换,反射帮你扒开 HashMap 的桶数组看个究竟”
java·数据结构·算法
w_zero_one2 小时前
算法--模拟
java·数据结构·算法
秋天的怀念_David3 小时前
算法一千零一夜 · 第十三夜 幻方,武侠世界里的数学迷宫
java·程序人生·算法
秋天的怀念_David3 小时前
算法一千零一夜 · 第十四夜 幻方与奇妙的二进制
java·程序人生·算法
薛定e的猫咪3 小时前
(ICLR 2020)MAML 靠的不是快速学习,而是特征复用
人工智能·深度学习·学习·算法·机器学习
薛定e的猫咪4 小时前
(ICLR 2026)DF-META:面向 FJSP 的无导数元学习实例自适应调度
人工智能·深度学习·学习·算法·机器学习
小羊没烦恼!5 小时前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#
wanderist.6 小时前
202609CSP认证P3
算法
云淡风轻~窗明几净6 小时前
角谷猜想最接近严格证明的测试 Version1.1.1
人工智能·算法·机器学习·图论
机器学习之心6 小时前
PINN × SVM × NSGA-II × 熵权TOPSIS:一套跑通「建模—优化—决策」的多目标优化框架
算法·机器学习·支持向量机