LeetCode 279. 完全平方数

题目描述

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

完全平方数是某个整数的平方,例如 14916

例如:

复制代码
输入: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 返回 0j > 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)
相关推荐
典典分享指南2 小时前
VS Code Git 工作树:解锁多分支并行开发新体验
算法·决策树·逻辑回归·启发式算法
_Narcissus_2 小时前
枚举和模拟算法笔记
c语言·数据结构·c++·笔记·算法·模拟·枚举
天疆说2 小时前
策略的进化:从随心所欲到稳健前行
算法
冻柠檬飞冰走茶3 小时前
《数据结构实验指导-C++语言版》 在顺序表 list 中查找元素 x
开发语言·数据结构·c++·算法·list
冻柠檬飞冰走茶3 小时前
《数据结构实验指导-C++语言版》 返回单链表 list 中第 i 个元素值
开发语言·数据结构·c++·算法·list
happyprince4 小时前
03-深刻观-CodeX哲学与升华(源码)
算法·ai编程
不会打球的王子4 小时前
Day 27:迁移学习与微调 — 站在巨人的肩膀上
算法
数模竞赛Paid answer4 小时前
2025年中青杯数学建模A题康养城市建设求解全过程论文及程序
算法·数学建模·数据分析·中青杯
网安蟹佬霸5 小时前
密码学安全实战:从加密原理到哈希破解的完整攻防指南
网络·算法·安全·web安全·开源·密码学·哈希算法