完全平方数 图解题解
这道题到底在问什么
- n
- 12
- 输出
- 3,因为 12 = 4 + 4 + 4
先想最直接的笨办法
想凑出 x,就枚举最后放下的那枚平方数 sq;放下它之前已经凑好 x−sq,所以 dp[x] = dp[x−sq] + 1,对所有可用的 sq 取最小。(动画第 4 步)
最优解:一步一步想明白
- 3拿 n=12 试贪心:先减最大的 9,剩 3,再减 1、1、1,一共 4 枚。但 4+4+4 只要 3 枚。贪心错了,因为最大平方数不一定在最优解里。
- 4想凑出 x,就枚举最后放下的那枚平方数 sq;放下它之前已经凑好 x−sq,所以 dp[x] = dp[x−sq] + 1,对所有可用的 sq 取最小。
- 5dp[0]=0(凑出 0 需要 0 枚),其余暂记 ∞凑出 0 不需要任何平方数,所以 dp[0]=0。其余位置先填无穷,表示「还没找到办法」。
- 6只用 1 时,dp[x] = dp[x−1] + 1 = x第一枚硬币只用 1:凑 4 就放四个 1,dp[4]=4。全用 1 一定能凑出来,但枚数最多——这是保底解,后面更大的平方数来把它压小。
- 7dp[4] = min(旧 4, dp[0] + 1) = 1现在轮到平方数 4。凑 4 时,回头看 dp[4−4]=dp[0]=0,再 +1 就是一枚 4。比原来的 4 枚 1 好太多,dp[4] 从 4 降到 1。
- 8dp[8] = min(旧 8, dp[4] + 1) = 2这是 sq=4 这一轮从左往右刚算到 dp[8] 的中途快照:凑 8 时回看 dp[8−4]=dp[4]=1,再加一枚 4,就是 4+4=两枚。它右边的 dp[9..12] 还停在上一轮全用 1 的旧值(9、10、11、12),扫描的指针还没走到,等会儿才轮到它们被压小。
- 9dp[12] = min(用 4 这条, 旧值)凑 12 时回看 dp[12−4]=dp[8]=2,再加一枚 4,就是 4+4+4=三枚。dp[12] 从 12 直接掉到 3。这就是动态规划的核心动作:站在大问题的肩膀上回看小问题。
- 10dp[9] 被 9 压到 1,但 dp[12] 守住 3最后一枚平方数 9 上场:dp[9] 被压到 1。再试 dp[12] 走 9 这条=dp[3]+1=4,没有 3 好,所以 dp[12] 守住 3。扫完所有平方数,dp[12]=3 就是答案。
- 13完全背包的灵魂:站在 x,枚举最后一枚平方数 sq,答案就是 dp[x−sq]+1 取最小。记住这句,零钱兑换、组合总和都是它。
- 15贪心 9+1+1+1 = 4 枚 > DP 的 3 枚贪心先抓最大的 9,剩下 3 只能补三个 1,合计 4 枚。DP 不迷信最大值,老老实实比较两条路,选出 4+4+4 这条只要 3 枚。这就是为什么这题必须用 DP。
- 19完全平方数是「完全背包求最少个数」的原子题。把硬币换成任意面额就是零钱兑换 322,把 min 换成计数就是组合总和 IV。想吃透这一类,去专题里连刷,卡住就问小欧或开通关训练。
⚠️ 容易写错的地方
✗ 错:用贪心:每次减最大的平方数
✓ 对:用 DP 枚举每枚平方数取最小
n=12 贪心 9+1+1+1=4 枚,DP 4+4+4=3 枚,贪心错
✗ 错:内层容量倒序遍历(当成 0/1 背包)
✓ 对:内层正序:for x in range(sq, n+1)
平方数可重复用是完全背包,正序才能让同一枚 sq 累加多次
✗ 错:无穷设成 INT_MAX 后还 +1
✓ 对:设 10^9 这种安全大数,或先判够不够再转移
INT_MAX+1 会整型溢出变负数,min 取到错误值
完整代码(Python / C++ / Java)
Python
class Solution:
def numSquares(self, n: int) -> int:
dp = [0] + [10**9] * n # dp[0]=0, 其余无穷
squares = [i*i for i in range(1, int(n**0.5)+1)]
for sq in squares: # 枚举每个平方数(硬币)
for x in range(sq, n + 1): # 正序=完全背包,可重复用
dp[x] = min(dp[x], dp[x - sq] + 1)
return dp[n]C++
class Solution {
public:
int numSquares(int n) {
vector<int> dp(n + 1, 1e9);
dp[0] = 0;
for (int i = 1; i * i <= n; ++i) { // 平方数 i*i
int sq = i * i;
for (int x = sq; x <= n; ++x) // 正序遍历容量
dp[x] = min(dp[x], dp[x - sq] + 1);
}
return dp[n];
}
};Java
class Solution {
public int numSquares(int n) {
int[] dp = new int[n + 1];
Arrays.fill(dp, 1_000_000_000); // 安全大数当无穷,+1 不溢出
dp[0] = 0;
for (int i = 1; i * i <= n; i++) { // 平方数 i*i
int sq = i * i;
for (int x = sq; x <= n; x++) // 正序遍历容量
dp[x] = Math.min(dp[x], dp[x - sq] + 1);
}
return dp[n];
}
}套路模板 · 完全背包求最少个数(挖空骨架)
# 完全背包·求最少个数 通用骨架(把 ___ 填上即可迁移)
dp = [0] + [INF] * target # dp[0]=0 是地基
for item in items: # ___: 可重复用的物品(平方数/硬币面额)
for x in range(item, target+1): # 正序=完全背包(可重复)
dp[x] = min(dp[x], dp[x-item] + 1) # +1=放下这一个 item
return dp[target] if dp[target] < INF else -1 # 凑不出返回 -1// 完全背包·求最少个数 通用骨架
vector<int> dp(target + 1, INF);
dp[0] = 0;
for (int item : items) // 可重复用的物品
for (int x = item; x <= target; ++x) // 正序
dp[x] = min(dp[x], dp[x - item] + 1);
return dp[target] < INF ? dp[target] : -1;// 完全背包·求最少个数 通用骨架
int[] dp = new int[target + 1];
Arrays.fill(dp, INF); dp[0] = 0;
for (int item : items) // 可重复用的物品
for (int x = item; x <= target; x++) // 正序
dp[x] = Math.min(dp[x], dp[x - item] + 1);
return dp[target] < INF ? dp[target] : -1;复杂度
时间复杂度
O(n√n)
外层平方数只有 √n 个(i*i ≤ n),内层从 sq 扫到 n 约 n 次,相乘 n√n
空间复杂度
O(n)
一维 dp 数组长度 n+1,正序遍历容量做到原地滚动,不需要二维表
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 完全平方数 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
外层枚举平方数、内层枚举容量,两层循环能交换吗?+
能。这题求「最少枚数」,dp[x] 只取 min,与遍历顺序无关,外内层交换结果一样。但若是求「组合数」或「排列数」,顺序就会改变语义,必须分清。
有没有 O(n) 或更快的解法?+
有。数学上的四平方和定理:任何正整数最多用 4 个平方数表示。按定理特判可做到接近 O(√n):答案只可能是 1、2、3、4,逐一判断即可。DP 是更通用、更好记的写法。
还能用 BFS 做吗?复杂度怎样?+
能。把 0 到 n 看成图的节点,x 到 x+sq 连边,从 0 做 BFS 求到 n 的最短层数,就是最少枚数。最坏复杂度同样是 O(n√n),但 BFS 一旦碰到 n 就能提前返回,常数更小。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。