当前位置: 首页 > news >正文

【LeetCode 热题 100】279. 完全平方数——(解法一)记忆化搜索

Problem: 279. 完全平方数

文章目录

  • 整体思路
  • 完整代码
  • 时空复杂度
    • 时间复杂度:O(n * sqrt(n))
    • 空间复杂度:O(n * sqrt(n))

整体思路

这段代码旨在解决一个经典的数论与动态规划问题:完全平方数 (Perfect Squares)。问题要求找到和为 n 的最少完全平方数(例如 1, 4, 9, 16, ...)的数量。

该算法采用的是 自顶向下(Top-Down)的动态规划,即 记忆化搜索 (Memoization)。它巧妙地将原问题转化为一个经典的组合优化模型:完全背包问题 (Unbounded Knapsack Problem)

  1. 问题转化:完全背包模型

    • 背包容量 (Capacity):目标整数 n
    • 物品 (Items):所有小于等于 n 的完全平方数,即 1^2, 2^2, 3^2, ..., i^2 其中 i*i <= n
    • 物品价值/重量:在这个问题中,我们不关心物品的“价值”,而是关心物品的“数量”。可以认为每个物品的“价值”是1。
    • 目标:用最少的物品(完全平方数)恰好装满背包(使它们的和等于 n)。
    • “完全/无界”的含义:每种物品(每个完全平方数)都可以无限次地使用。例如,12 = 4 + 4 + 4
  2. 状态定义与递归关系

    • 算法的核心是递归函数 dfs(i, j),其状态定义为:
      dfs(i, j) = 使用物品 1^2, 2^2, ..., i^2 来凑成和为 j 时,所需要的最少物品数量。
    • 对于当前状态 dfs(i, j),我们面临一个选择,即是否使用物品 i^2
      a. 不使用 i^2:如果我们决定不使用 i^2,那么问题就变成了“使用物品 1^2, ..., (i-1)^2 来凑成和为 j”,这对应于递归调用 dfs(i-1, j)
      b. 使用 i^2:如果我们决定使用 i^2(前提是 j >= i*i),那么我们已经用掉了一个物品,剩下的问题是“使用物品 1^2, ..., i^2 来凑成和为 j - i*i”。注意这里仍然是 dfs(i, ...) 而不是 dfs(i-1, ...),因为这是完全背包问题,i^2 可以重复使用。这对应于 dfs(i, j - i*i) + 1
    • dfs(i, j) 的最终结果就是这两种选择中的最小值。
  3. 记忆化与基础情况

    • 记忆化:为了避免重复计算,算法使用了一个二维数组 memomemo[i][j] 存储 dfs(i, j) 的结果。在函数开头检查 memo 可以将时间复杂度从指数级降至多项式级。
    • 基础情况if (i == 0) 是递归的出口。当没有物品可用时(i=0),如果目标和 j 恰好也是0,说明成功了,用了0个物品;否则,无法凑成,返回一个极大值 Integer.MAX_VALUE 表示此路不通。

完整代码

import java.util.Arrays;class Solution {// memo: 记忆化数组。memo[i][j] 存储用 1^2...i^2 凑成 j 所需的最少平方数。// 声明为 static,使得 memo 表在所有 Solution 对象实例之间共享,并能在 static 块中初始化。// 题目约束 n <= 10000,所以 sqrt(n) <= 100。private static final int[][] memo = new int[101][10001];// static 初始化块:在类加载时执行一次,用于初始化 memo 数组。static {for (int[] row : memo) {// 将所有值填充为 -1,表示该状态尚未计算。Arrays.fill(row, -1);}}/*** 计算和为 n 的最少完全平方数的数量。* @param n 目标整数* @return 最少完全平方数的数量*/public int numSquares(int n) {// 启动递归。// 第一个参数是可用的最大平方数的底数,即 sqrt(n)。// 第二个参数是目标和 n。return dfs((int) Math.sqrt(n), n);}/*** 记忆化搜索函数。* @param i 表示当前可以使用的最大平方数是 i*i。* @param j 表示当前需要凑成的目标和。* @return 凑成 j 所需的最少平方数数量。*/private int dfs(int i, int j) {// 基础情况(递归出口):当没有平方数可用时 (i=0)if (i == 0) {// 如果目标和 j 也是 0,说明成功凑出,用了0个数。// 否则,无法凑出,返回一个极大值表示此路不通。return j == 0 ? 0 : Integer.MAX_VALUE;}// 记忆化检查:如果 memo[i][j] 不为 -1,说明这个子问题已计算过,直接返回结果。if (memo[i][j] != -1) {return memo[i][j];}// 剪枝优化:如果当前目标和 j 小于当前考虑的平方数 i*i,// 那么 i*i 肯定不能用。问题直接退化为用 1^2...(i-1)^2 去凑 j。if (j < i * i) {return memo[i][j] = dfs(i - 1, j);}// 状态转移:// 我们在两种选择中取最小值:// 1. dfs(i - 1, j):      不使用 i*i 这个数。// 2. dfs(i, j - i * i) + 1: 使用至少一个 i*i。return memo[i][j] = Math.min(dfs(i - 1, j), dfs(i, j - i * i) + 1);}
}

时空复杂度

时间复杂度:O(n * sqrt(n))

  1. 状态数量:由于记忆化的存在,每个状态 (i, j) 只会被实际计算一次。
    • i 的范围是从 0(int)Math.sqrt(n)
    • j 的范围是从 0n
    • 因此,总的状态数量是 (sqrt(n) + 1) * (n + 1)
  2. 每个状态的计算时间:在 dfs 函数内部,除了递归调用,其他的操作(比较、加减、Math.min)都是 O(1) 的。

综合分析
总的时间复杂度等于(状态数量)×(每个状态的计算时间),即 O(sqrt(n) * n)。

空间复杂度:O(n * sqrt(n))

  1. 记忆化数组:算法创建了一个 memo 二维数组来存储所有子问题的解。其大小为 101 * 10001,对应于 (sqrt(n_max) + 1) * (n_max + 1)。因此,这部分空间占用为 O(n * sqrt(n))
  2. 递归调用栈:递归的深度也需要考虑。在最坏的情况下,例如 dfs(1, n),递归链可能是 dfs(1, n) -> dfs(1, n-1) -> ...,深度可达 O(n)。另一个方向是 dfs(sqrt(n), n) -> dfs(sqrt(n)-1, n) -> ...,深度是 O(sqrt(n))。最大深度是 O(n)。
  3. 综合分析
    • 记忆化数组的空间开销是 O(n * sqrt(n))。
    • 递归栈的空间开销是 O(n)。
    • 由于 n * sqrt(n) 是主导项,因此最终的空间复杂度为 O(n * sqrt(n))

参考灵神

http://www.dtcms.com/a/338836.html

相关文章:

  • kkfileview预览Excel文件去掉左上角的跳转HTM预览、打印按钮
  • Python爬虫第一课:爬取HTML静态网页小说章节
  • 国产!全志T113-i 双核Cortex-A7@1.2GHz 工业开发板—ARM + FPGA通信案例
  • PicoShare 文件共享教程:cpolar 内网穿透服务实现跨设备极速传输
  • Simulink库文件创建及使用方法
  • 4.Kotlin 集合 Map 所有方法
  • CVPR 2025 | 具身智能 | HOLODECK:一句话召唤3D世界,智能体的“元宇宙练功房”来了
  • 库卡机器人tag焊接保护气体流量控制系统
  • (第五篇)spring cloud之Ribbon负载均衡
  • 主流 3D 模型格式(FBX/OBJ/DAE/GLTF)材质支持与转换操作指南
  • 云存储的高效安全助手:阿里云国际站 OSS
  • ICCV 2025 | 首个3D动作游戏专用VLA模型,打黑神话只狼超越人类玩家
  • iOS 性能监控实践,如何构建从开发到运维的持续优化体系
  • 面试题储备-MQ篇 3-说说你对Kafka的理解
  • 如何用给各种IDE配置R语言环境
  • Halcon联合C# 添加工具类让winform自动根据窗体大小自适应缩放所有控件
  • 知行社黄剑杰:金融跨界,重塑震区救援新章
  • 《基于大数据的全球用水量数据可视化分析系统》用Python+Django开发,为什么导师却推荐用Java+Spring Boot?真相揭秘……
  • sqli-labs通关笔记-第55关 GET数值型注入(括号闭合 限制14次探测机会)
  • 今日行情明日机会——20250819
  • 20.2 QLoRA微调全局参数实战:高点击率配置模板+显存节省50%技巧
  • Linux下Nginx安装及负载均衡配置
  • Python 3.14深度解析:革命性特性与性能优化实践
  • Go高效复用对象:sync.Pool详解
  • Windows内核开发笔记
  • 免费导航规划API接口详解:调用指南与实战示例
  • 一个基于前端技术的小狗寿命阶段计算网站,帮助用户了解狗狗在不同年龄阶段的特点和需求。
  • 数据链路层-网络层-传输层
  • js 值转换boolean方式
  • AutoSAR实战:DCM配置之Response On Event (0x86)事件响应配置指导