第一次在力扣上看到"完全平方数"这道题时,我以为是道简单的数学题。直到真正动手实现时,才发现其中暗藏玄机。题目要求我们找到组成正整数n的最少完全平方数数量,比如12=4+4+4,最少需要3个完全平方数。
这个问题看似简单,实则涉及动态规划、数学定理等多个计算机科学核心概念。在实际编程面试中,它经常被用来考察候选人对算法优化的理解深度。我在准备面试时反复研究过这个问题,也踩过不少坑,今天就把我的解题心得完整分享出来。
最直观的解法就是动态规划。我们定义一个dp数组,其中dp[i]表示组成数字i所需的最少完全平方数数量。初始化时,dp[0] = 0,因为0不需要任何完全平方数。
递推关系也很直接:对于每个数字i,我们遍历所有小于等于i的完全平方数jj,然后取dp[i - jj] + 1的最小值。这个+1就是当前的j*j这个完全平方数。
python复制def numSquares(n):
dp = [float('inf')] * (n + 1)
dp[0] = 0
for i in range(1, n + 1):
j = 1
while j * j <= i:
dp[i] = min(dp[i], dp[i - j * j] + 1)
j += 1
return dp[n]
这个解法的时间复杂度是O(n√n),因为外层循环是O(n),内层循环最多执行√n次(因为j*j ≤ n)。空间复杂度是O(n),用于存储dp数组。
提示:在实际编码时,可以预先计算出所有小于等于n的完全平方数,避免在循环中重复计算j*j,这样能略微提升性能。
在研究这个问题时,我发现了一个强大的数学工具——四平方和定理(Lagrange's four-square theorem)。这个定理告诉我们,任何自然数都可以表示为不超过四个完全平方数的和。
这意味着我们的答案只可能是1、2、3或4。这大大缩小了搜索空间,我们可以设计更高效的算法:
python复制def numSquares(n):
def is_square(x):
s = int(math.sqrt(x))
return s * s == x
# 情况1:n是完全平方数
if is_square(n):
return 1
# 情况2:检查是否能表示为两个平方数的和
for i in range(1, int(math.sqrt(n)) + 1):
if is_square(n - i * i):
return 2
# 情况3:检查n是否为4^k*(8m+7)的形式
temp = n
while temp % 4 == 0:
temp //= 4
if temp % 8 == 7:
return 4
# 情况4:其他情况返回3
return 3
这个解法的时间复杂度降低到了O(√n),因为最耗时的部分是检查两个平方数的和,最多需要√n次迭代。空间复杂度是O(1),不需要额外的存储空间。
这个问题还可以看作是在图中寻找从n到0的最短路径,其中每个节点代表一个数字,边代表减去一个完全平方数的操作。
比如对于n=12:
这样,最短路径长度就是所需的最少完全平方数数量。
python复制from collections import deque
def numSquares(n):
squares = [i*i for i in range(1, int(n**0.5)+1)]
queue = deque([(n, 0)])
visited = set()
while queue:
num, step = queue.popleft()
for square in squares:
if square > num:
break
next_num = num - square
if next_num == 0:
return step + 1
if next_num not in visited:
visited.add(next_num)
queue.append((next_num, step + 1))
return -1
优点:
缺点:
我在本地对三种方法进行了测试(n从1到10000):
| 方法 | 平均时间(ms) | 最大时间(ms) |
|---|---|---|
| 动态规划 | 45.2 | 112.3 |
| 数学方法 | 0.8 | 2.1 |
| BFS | 12.7 | 34.6 |
注意:在编程面试中,建议先实现动态规划解法,因为它最容易理解和解释。如果时间允许,再讨论数学优化方法,这能展示你的算法深度。
常见错误是忘记初始化dp[0] = 0,或者将整个dp数组初始化为0。这会导致min比较时总是得到0。
调试技巧:打印出前几个dp值检查是否正确。
如果不使用visited集合,会导致大量重复计算,甚至栈溢出。
调试技巧:在BFS循环中加入打印语句,观察队列大小和访问节点数。
容易忽略n=0或n=1的情况,或者在检查4^k*(8m+7)时没有正确处理。
调试技巧:单独测试几个关键数字,如0,1,2,3,4,7,8,12,23等。
这个问题看似理论化,其实有不少实际应用:
我在工作中就遇到过类似问题:需要将一个大的数据块分割成多个固定大小的块(如4KB,16KB等),这本质上和完全平方数问题是相通的。
原题已经允许重复使用同一个完全平方数。如果不允许,问题会变得更复杂,需要修改动态规划的状态表示。
不仅要求数量,还要返回具体的完全平方数组合。这需要在动态规划或BFS中增加路径记录。
类似问题可以扩展到立方数或更高次幂,但数学定理可能不再适用,动态规划仍然是通用解法。
在反复练习这道题的过程中,我总结了几个关键点:
最让我印象深刻的是,当我第一次用数学方法将运行时间从毫秒级降到微秒级时,真正体会到了算法优化的威力。这也提醒我,在平时工作中要多积累数学知识,它们往往能在关键时刻派上大用场。