LeetCode 279:完全平方数(完全背包)—— 题解
欢迎阅读 欢迎来到「完全平方数」题解之旅本文将带你从“用最少的平方数拼出给定整数”这一数学问题出发深入理解完全背包求最小值的 DP 模型并掌握如何将隐式物品平方数动态生成融入背包框架。在开始之前建议你先了解题目背景这是 LeetCode 279 题给定整数 nn求最少需要多少个完全平方数1, 4, 9, 16, ...相加得到 nn。本质上我们可以把每个平方数看作一种无限量供应的物品体积为该平方数的值价值为 1即硬币个数目标是用最少的物品凑满容量为 nn 的背包。明确学习目标掌握如何将“最少平方数数量”转化为完全背包求最小值理解物品列表的动态生成只需用到 1212 到 ⌊n⌋2⌊n​⌋2并熟练使用INF 初始化不可达状态以及dp[0] 0的起点设定。准备好环境建议在本地 IDE 或 LeetCode 在线编辑器中打开代码边看边运行亲手验证示例如 n12n12 输出 3对应 444444。本文将从问题转化、状态定义、转移方程、初始化、填表顺序到代码实现层层递进。即使你对完全背包还不熟悉我们也会从“零钱兑换”的类比出发让你轻松抓住核心思想——平方数即硬币的建模技巧以及正序内层循环在完全背包中的关键作用。现在让我们一起用最少的平方数拼出目标解开完全平方数的 DP 密码吧 ✨一、题目279. 完全平方数 - 力扣LeetCode二、做题思路问题转化前置分析本题要求用最少的完全平方数可重复使用之和凑成整数n。将问题转化为完全背包求最小值背包容量 n物品所有完全平方数1^2, 2^2, ..., (√n)^2每种数量无限物品体积 平方数的值物品价值 1每个平方数贡献 1 个数量目标恰好装满容量n最小化总价值平方数个数不可达状态用INF大数标记。1. 状态表示核心基础定义dp[i][j]表示使用前i个平方数即1^2, 2^2, ..., i^2凑成整数j所需的最少个数。i范围0到rowrow ⌊√n⌋表示平方数种类数j范围0到n。2. 状态转移方程关键难点对于第i个平方数值为square i*i有两种决策不选个数为dp[i-1][j]继承上一行的结果。选至少一枚前提是j square个数为dp[i][j - square] 1。注意此处使用的是dp[i][...]而非dp[i-1][...]因为完全背包允许重复选取当前平方数dp[i][j - square]可能已经包含了当前平方数的多次使用这体现了无限取用的特性。取两者最小值dp[i][j] min(dp[i-1][j], (j square ? dp[i][j - square] 1 : INF))。3. 初始化边界防护dp[0][0] 0不使用任何平方数凑出整数0需要0个可行。dp[0][j] INFj 0没有平方数可用时无法凑出任何正数标记为不可达。代码中通过循环for (int j1; jn; j) dp[0][j] INF;实现dp[0][0]保持默认0。其余dp[i][j]初始化为0但会在递推中被覆盖。4. 填表顺序递推方向dp[i][j]依赖上一行dp[i-1][j]和当前行左侧dp[i][j - square]因此必须按行从上到下i从 1 到row列从左到右j从 1 到n遍历。5. 返回值目标映射最终返回dp[row][n]即使用所有平方数凑成整数n的最少个数。若不可达理论上不会因为1是平方数总能凑出则返回INF但题目保证n1所以结果总是有限的。三、代码class Solution { public: int numSquares(int n) { // 将问题转化为完全背包 // 物品所有平方数 1^2, 2^2, ..., (sqrt(n))^2每种数量无限 // 背包容量n // 目标用最少的物品数恰好装满背包 int row sqrt(n); // 平方数的个数即物品种类数 int col n; // 背包容量 const int INF 0x3f3f3f3f; // 1. 创建dp表 // dp[i][j] 表示使用前 i 个平方数1^2 ~ i^2凑成整数 j 所需的最少个数 vectorvectorint dp(row 1, vectorint(col 1)); // 2. 初始化 // dp[0][0] 0不使用任何平方数凑成0需要0个 // dp[0][j] INFj0时不使用任何平方数无法凑成设为不可达 for (int j 1; j col; j) { dp[0][j] INF; } // dp[0][0] 默认为0vector初始化已为0 // 3. 填表顺序外层遍历平方数种类i从1到row内层遍历容量j从1到col // 因为完全背包允许重复使用同一种平方数所以内层容量正序遍历 // 使得 dp[i][j - i*i] 已经考虑过当前平方数的多次使用。 for (int i 1; i row; i) { int square i * i; // 当前平方数的值 for (int j 1; j col; j) { // 4. 状态转移方程完全背包二维形式 // 不选当前平方数dp[i][j] dp[i-1][j] dp[i][j] dp[i - 1][j]; // 选当前平方数至少一次前提是 j square // 此时 dp[i][j - square] 1 表示用当前平方数补足剩余容量 // 取最小值更新。 if (j square) { dp[i][j] min(dp[i][j], dp[i][j - square] 1); } } } // 5. 返回值dp[row][col] 即为用所有平方数凑成 n 的最少个数 return dp[row][col]; } };四、流程图五、优化状态转移方程对于当前平方数square i*i第i种决策为选或不选但由于可重复选一维转移为dp[j] min(dp[j], dp[j - square] 1)当j square时。dp[j]左侧等号右边为上一轮不选当前平方数的值即继承旧状态。dp[j - square]为本轮已更新的值表示已经选过至少一枚当前平方数后继续累加的数量这允许无限次使用同一平方数。填表顺序外层循环遍历每种平方数i从 1 到row内层循环必须正序遍历整数j从 1 到n。为什么完全背包要正序从左到右因为完全背包允许无限次选取当前平方数dp[j]需要利用同一平方数已更新过的较小整数状态dp[j - square]即表示“已经选过一枚当前平方数后继续选”的累计数量。当j从小到大遍历时dp[j - square]已经在本轮被更新过因为j - square j先被处理所以它包含了当前平方数的多次使用信息从而允许无限取用。若采用逆序如01背包dp[j - square]仍为上一轮状态则每个平方数最多被选一次无法实现重复选取结果错误例如n12时只能选444需要三次逆序会漏算。与01背包逆序的对比01背包中每个物品只能选一次必须保证dp[j - square]是上一轮状态因此需要逆序遍历容量避免覆盖。class Solution { public: int numSquares(int n) { int row sqrt(n); // 平方数种类数1^2 ~ row^2 int col n; const int INF 0x3f3f3f3f; // 一维滚动数组 dp[j]表示当前已考虑的平方数种类下凑成金额 j 的最少个数 // 初始时未考虑任何平方数仅 dp[0]0 可达0个其余为 INF不可达 vectorint dp(col 1); for (int j 1; j col; j) { dp[j] INF; } // 外层遍历平方数种类相当于完全背包的物品内层正序遍历容量 // 正序使得 dp[j - i*i] 在本轮已被更新允许同一平方数重复使用完全背包特性 for (int i 1; i row; i) { int square i * i; for (int j 1; j col; j) { if (j square) { // dp[j] 保留旧值不选当前平方数 // 或从 dp[j - square] 1 转移选一个当前平方数复用本轮更新的结果 dp[j] min(dp[j], dp[j - square] 1); } } } return dp[col]; } }; 闭幕 恭喜你完成了「完全平方数」问题的学习为了巩固知识并进一步拓展建议你动手实践在 LeetCode 上提交代码尝试不同的测试用例。深入思考本题将完全平方数视为物品n视为背包容量求最少数量这属于完全背包的恰好装满问题。代码中dp[0][0]0其他dp[0][j]INF为什么这样初始化如果把dp[0][j]也设为0输出会有什么变化物品列表只取到sqrt(n)的平方数为什么不需要考虑更大的平方数如(sqrt(n)1)^2本题直接用数学方法四平方和定理也能求解但 DP 更加通用。你觉得 DP 和定理法各自的优缺点是什么如果n非常大如10^9sqrt(n)也很大DP 会超时你能想到哪些优化思路延伸挑战将题目改为用完全平方数凑成 n 的组合数不同顺序视为同一种类比零钱兑换 II状态转移和初始化应如何调整如果每个完全平方数最多只能用一次即 01 背包代码只需改动哪一处动手改一改并验证n12时结果会变成多少。考虑最少数量的同时如果还要输出具体的平方数组合如12444你如何在 DP 过程中记录路径并回溯如果你觉得本文对你有所帮助欢迎 点赞 / 收藏 关注作者获取更多题解 留言交流你的疑问或优化思路祝你在DP 的道路上越走越稳早日攻克每一道难题下次见 ✨

相关新闻

Cursor学生认证全流程指南(含审核失败率TOP3原因):0元解锁Pro功能的最后窗口期

Cursor学生认证全流程指南(含审核失败率TOP3原因):0元解锁Pro功能的最后窗口期

更多请点击: https://codechina.net 第一章:Cursor学生认证全流程指南(含审核失败率TOP3原因):0元解锁Pro功能的最后窗口期 Cursor 学生认证是目前唯一官方支持的永久免费获取 Pro 功能(包括 AI 补全增强、…

2026/10/28 15:35:21 阅读更多 →
SolidWorks快捷键全攻略:从入门到精通,效率提升200%

SolidWorks快捷键全攻略:从入门到精通,效率提升200%

如果你是一名机械设计师、工程师或学生,每天在 SolidWorks 中花费数小时进行建模、装配和出图,那么掌握快捷键可能是你提升效率最快、最直接的方式。这无关乎软件版本,而是一种工作习惯的彻底革新。今天这篇文章,我们不谈复杂的宏…

2026/11/2 19:46:46 阅读更多 →
智能体技能体系解析:从概念到实战,构建自动化工作流

智能体技能体系解析:从概念到实战,构建自动化工作流

你有没有遇到过这种情况:花了不少时间,终于把一个看起来很酷的AI工具装好了,界面也打开了,但面对满屏的选项和按钮,却不知道从哪里下手,更别说让它真正帮你干活了。最后,工具还是那个工具&#…

2026/11/2 3:19:55 阅读更多 →

最新新闻

Electron桌面应用开发实战:从环境搭建到打包发布全流程

Electron桌面应用开发实战:从环境搭建到打包发布全流程

做前端这些年,我大部分时间都在跟浏览器标签页打交道,直到第一次把桌面应用跑起来,才发现原来自己一直被限制在地址栏和刷新按钮围起来的框框里。Electron这个词,最开始是在某段时间频繁出现在同事的讨论里,一个用HTML…

2026/11/2 0:25:10 阅读更多 →
滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

滑盖智能锁T109深度解析:结构原理、安装调试与故障排查

简介:亚太天能T109滑盖智能锁产品说明书是一份PDF格式的官方技术文档,主要面向智能锁安装人员、终端用户及售后维护者,帮助快速掌握产品安装、配置与常见故障处理。全包仅1个PDF文件,大小4.37MB,内容结构清晰&#xff…

2026/11/2 10:46:11 阅读更多 →
PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

PMSM弱磁控制原理与Simulink建模:高速AGV驱动系统设计指南

上个月我在调试一台高速AGV的驱动系统,整车要求空载跑到8m/s,电机转速已经拉到额定点,驱动器的输出电压也顶到了母线电压极限附近。再往上踩速度指令,转速纹丝不动,三相电流反而开始震荡。那会儿我的第一反应就是&…

2026/11/2 10:47:40 阅读更多 →
C++核心知识点:从内存管理到STL的实战指南

C++核心知识点:从内存管理到STL的实战指南

C 这门语言我前前后后折腾了十几年,用它写过嵌入式驱动、做过游戏服务端,也搭过基础组件库。每次看到新人一头扎进"C 核心知识点"这个搜索词,我其实挺感慨的——这门语言的难点从来不是语法多,而是知识点之间的耦合太深…

2026/11/2 1:16:51 阅读更多 →
T109滑盖智能锁怎么装?安装调试与故障排查全攻略

T109滑盖智能锁怎么装?安装调试与故障排查全攻略

简介:亚太天能T109滑盖智能锁产品说明书以PDF格式发布,适合智能锁用户、安装人员与售后维修人员阅读。内容围绕T109系列指纹密码刷卡锁展开,先介绍前面板、后面板、锁体、机械钥匙、电池盒盖等部件名称与功能,再给出完整安装步骤&…

2026/11/2 10:46:31 阅读更多 →
LLM知识降熵:从信息垄断到理解权下放

LLM知识降熵:从信息垄断到理解权下放

你有没有过这种经历:翻开源码注释、论文摘要或者一份专业报告,每个字都认识,连在一起却像天书。想找人聊聊,身边要么没人懂,要么懂的懒得跟你解释。后来我试着把同一段内容丢给一个大语言模型,几秒钟之后&a…

2026/11/2 10:46:26 阅读更多 →

日新闻

周新闻

月新闻