好的,我已经记录了你已学习过的所有题目。现在,我给你随机出一道你列表中没有出现过的、经典的线性动态规划题目。
题目:完全平方数
题目描述:
给定一个正整数 n,你的任务是找到最少数量的完全平方数(例如 1, 4, 9, 16, …)使得它们的和等于 n。
换句话说,我们需要用最少数量的完全平方数来拼凑出整数 n。这是一个典型的“数字拆分”或“硬币找零”类问题,只不过这里的“硬币”是所有的完全平方数。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4。也可以表示为 12 = 9 + 1 + 1 + 1,但需要4个数,不是最少的。最少需要3个。
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9。
解题思路:循序渐进地讲解
我们可以把这个问题看作一个动态规划问题,状态定义为凑出某个金额 i 所需的最少完全平方数的数量。
步骤 1:定义状态
定义 dp[i] 为:凑出总和为 i 所需的最少完全平方数的个数。
我们的最终目标是求 dp[n]。
步骤 2:寻找状态转移方程
如何计算 dp[i] 呢?
要凑出总和 i,最后一个加入的完全平方数可能是 1, 4, 9, 16, …,当然,这个数不能大于 i。
- 如果我们最后加入的数是
1(即1*1),那么我们需要先凑出i - 1,然后再加1个1。所以方案数为dp[i-1] + 1。 - 如果我们最后加入的数是
4(即2*2),那么我们需要先凑出i - 4,然后再加1个4。所以方案数为dp[i-4] + 1。 - 如果我们最后加入的数是
9(即3*3),那么方案数为dp[i-9] + 1。 - ……
我们需要找到所有可能中数量最少的那个。
因此,状态转移方程为:
dp[i] = min( dp[i - j*j] + 1 ),其中 j*j <= i。
即,我们遍历所有小于等于 i 的完全平方数 j*j,看从 i - j*j 这个状态转移过来,哪个需要的数字个数最少。
步骤 3:确定初始条件
dp[0] = 0。凑出总和0,不需要任何数。- 对于其他
i,我们可以先初始化为一个很大的数(比如n+1),因为最多的情况就是全部用1来凑,需要i个。我们将在状态转移中更新它。
步骤 4:计算顺序
由于计算 dp[i] 时,需要用到 dp[i - j*j](这些值都比 i 小),所以我们按照 i 从 1 到 n 的顺序来计算。
步骤 5:举例说明(以 n=12 为例)
-
初始化:
dp = [0, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞],长度为n+1=13。dp[0]=0。 -
计算
i=1:
小于等于1的完全平方数只有1*1=1。
dp[1] = dp[1-1] + 1 = dp[0] + 1 = 0 + 1 = 1。
更新dp = [0, 1, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞] -
计算
i=2:
小于等于2的完全平方数只有1*1=1。
dp[2] = dp[2-1] + 1 = dp[1] + 1 = 1 + 1 = 2。(即2 = 1 + 1)
dp = [0, 1, 2, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞] -
计算
i=3:
小于等于3的完全平方数只有1。
dp[3] = dp[3-1] + 1 = dp[2] + 1 = 2 + 1 = 3。(即3 = 1 + 1 + 1)
dp = [0, 1, 2, 3, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞] -
计算
i=4:
小于等于4的完全平方数有1和4。- 用
1:dp[4-1] + 1 = dp[3] + 1 = 3 + 1 = 4 - 用
4:dp[4-4] + 1 = dp[0] + 1 = 0 + 1 = 1
取最小值min(4, 1) = 1。
所以dp[4] = 1。(即4 = 4)
dp = [0, 1, 2, 3, 1, ∞, ∞, ∞, ∞, ∞, ∞, ∞, ∞]
- 用
-
继续这个过程:
i=5: 检查1和4。
min(dp[4]+1=2, dp[1]+1=2) = 2。 (5=4+1)i=6: 检查1和4。
min(dp[5]+1=3, dp[2]+1=3) = 3。 (6=4+1+1)i=7: 检查1和4。
min(dp[6]+1=4, dp[3]+1=4) = 4。 (7=4+1+1+1)i=8: 检查1和4。
min(dp[7]+1=5, dp[4]+1=2) = 2。 (8=4+4)i=9: 检查1,4,9。
min(dp[8]+1=3, dp[5]+1=3, dp[0]+1=1) = 1。 (9=9)i=10: 检查1,4,9。
min(dp[9]+1=2, dp[6]+1=4, dp[1]+1=2) = 2。 (10=9+1)i=11: 检查1,4,9。
min(dp[10]+1=3, dp[7]+1=5, dp[2]+1=3) = 3。 (11=9+1+1)i=12: 检查1,4,9。
min(dp[11]+1=4, dp[8]+1=3, dp[3]+1=4) = 3。 (12=4+4+4)
-
最终结果:
dp[12] = 3,与示例一致。
步骤 6:算法实现(Python伪代码)
def numSquares(n: int) -> int:
# 初始化dp数组,dp[0]=0,其他为正无穷大
dp = [float(‘inf’)] * (n + 1)
dp[0] = 0
# 遍历所有需要计算的状态 i (从1到n)
for i in range(1, n + 1):
# 遍历所有可能的完全平方数 j*j <= i
j = 1
while j * j <= i:
# 状态转移:dp[i] 可能是由 dp[i - j*j] 加上一个平方数 j*j 得来
dp[i] = min(dp[i], dp[i - j * j] + 1)
j += 1
return dp[n]
步骤 7:复杂度分析
- 时间复杂度:
O(n * sqrt(n))。外层循环遍历n次,内层循环的j最大到sqrt(n)。 - 空间复杂度:
O(n),用于存储dp数组。
总结一下:
这道题的核心是将一个“求最值”的优化问题,分解成一系列子问题(dp[i] 表示凑出 i 的最优解)。通过分析最后一个加入的“成分”(一个完全平方数),我们建立了当前问题 dp[i] 与更小子问题 dp[i - j*j] 之间的联系,从而可以用自底向上的动态规划高效求解。