LC279 完全平方数¶
讲解章节:背包问题
一句话
把平方数当成面值无限供应的硬币,做一遍完全背包求最少个数。
解题思路¶
这题考什么¶
「用若干个数凑出目标值,每个数可以重复使用,求最少个数」正是完全背包的 最小化形态,见 背包问题。 这题的物品表是现成的:所有不超过 n 的完全平方数 1, 4, 9, ..., k*k。
状态定义:f[c] 表示把整数 c 拆成若干个完全平方数之和所需的最少个数。 转移方程(sq 遍历每个完全平方数):
初值 f[0] = 0(凑出 0 一个数都不用),其余取一个不可能达到的大数当无穷。
容量正序遍历是完全背包的标志,也是这题最该讲透的一点: 扫到 f[c] 时,f[c - sq] 有可能已经在本轮被同一个 sq 更新过, 于是「再用一个 sq」这件事被自然允许,同一个平方数可以取任意多次。 若把容量改成倒序,f[c - sq] 一定还是上一轮的旧值,每个平方数就只能用一次, 求出来是 01 背包的答案(n = 12 时凑不出来,f[12] 会停在哨兵上,见「坑在哪」第 3 条)。
另一种同样正确的思路是把它看成无权图上的最短路:从 n 出发,每次减去一个平方数, 求到 0 的最少步数,用广度优先搜索逐层扩展。两者的层数与背包的「个数」一一对应。
数据规模与复杂度¶
1 <= n <= 1e4。 平方数的个数是 sqrt(n) <= 100,每个平方数扫一遍容量最多 1e4 格, 时间 O(n * sqrt(n)) = 1e4 * 100 = 1e6 次内层迭代,本机实测 n = 1e4 约 0.05 秒; 判题机比本机慢数倍,仍留有充足余量。 空间 O(n),一个长度 n+1 的整数列表。
这题不需要无解出口:拉格朗日四平方和定理保证任何正整数都能写成至多 4 个 完全平方数之和,而且面值 1 本身就是完全平方数,逐个 1 相加总能凑出任何 n。 所以答案必然存在且不超过 4。
坑在哪¶
- 「无穷」取 n + 1 而不是 Python 的 float('inf')。 全用面值 1 至多需要 n 个,任何真实答案都不会超过 n, 所以 n + 1 一定达不到,足够当哨兵;同时保持整数运算,比浮点比较快。
- f[0] = 0 是递推的起点。改成 n + 1 会让所有格子都停在无穷,返回垃圾值。
- 内层容量必须正序。倒序会把完全背包退化成 01 背包,每个平方数只许用一次。 n = 12 时可用的平方数只有 1、4、9,任何子集之和都凑不出 12 (1+4+9 = 14、4+9 = 13、1+9 = 10),f[12] 会一直停在哨兵值 13 上, 函数返回 13 这个垃圾数——而正确答案是 4 + 4 + 4 = 3。
- while 的条件是 k * k <= n 而不是 k <= n。写成后者会枚举出大于 n 的平方数, 那些 sq 让内层 range 为空,纯属浪费,n = 1e4 时白转 9900 圈。
- 不要试图用「贪心地每次减去最大的平方数」。n = 12 时贪心会取 9 + 1 + 1 + 1 = 4 个, 而最优是 4 + 4 + 4 = 3 个。这类「凑数取最少」的问题在一般面值下贪心不成立, 必须 DP。
样例复核¶
n = 12。物品是 1、4、9。
只用 1 时 f[12] = 12;
加入 4 后正序扫描:f[4] = 1, f[8] = f[4] + 1 = 2, f[12] = f[8] + 1 = 3
(这三次更新落在同一轮内层循环里,正是正序遍历允许重复取用的体现);
加入 9 后 f[12] 的候选是 f[3] + 1 = 3 + 1 = 4,不优于 3。
返回 3,与期望输出 12 = 4 + 4 + 4 一致。