按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度: 1. 二叉树的中序遍历 - 增加"思路推导"部分,解释递归到迭代的转换 - 详细说明迭代法的每个步骤 - 增加执行过程演示和多种解法 2. 二叉树的最大深度 - 增加"思路推导",对比DFS和BFS - 详细解释递归的基准情况 - 增加多种解法和变体问题 3. 从前序与中序遍历序列构造二叉树 - 详细解释前序和中序的特点 - 增加"思路推导",说明如何分治 - 详细说明切片边界计算 4. 对称二叉树 - 解释镜像对称的定义 - 详细说明递归比较的逻辑 - 增加迭代解法和变体问题 5. 翻转二叉树 - 解释翻转的定义和过程 - 详细说明多值赋值的执行顺序 - 增加多种解法和有趣的故事 6. 路径总和 - 详细解释路径和叶子节点的定义 - 说明为什么使用递减而非累加 - 增加多种解法和变体问题 每个文件都包含: - 完整的示例和边界条件分析 - 详细的算法流程和图解 - 关键细节说明 - 常见错误分析 - 复杂度分析(详细版) - 执行过程演示 - 多种解法 - 变体问题 - 总结 Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
14 KiB
完全平方数 (Perfect Squares)
LeetCode 279. Medium
题目描述
给你一个整数 n,返回和为 n 的完全平方数的最少数量。
完全平方数:一个整数等于其平方的整数,如 1, 4, 9, 16 等。
示例 1:
输入:n = 12
输出:3
解释:12 = 4 + 4 + 4
示例 2:
输入:n = 13
输出:2
解释:13 = 4 + 9
示例 3:
输入:n = 1
输出:1
解释:1 = 1
思路推导
暴力解法分析
最直观的思路:尝试所有可能的完全平方数组合。
def numSquares(n):
if int(n**0.5)**2 == n:
return 1
# 尝试两个数的和
for i in range(1, int(n**0.5) + 1):
if int((n - i*i)**0.5)**2 == n - i*i:
return 2
# 尝试三个数的和
# ...(代码会非常复杂)
# 根据拉格朗日四平方定理,最多需要 4 个数
return 4
时间复杂度:难以估计,取决于实现
- 最坏情况可能需要枚举所有组合
- 代码复杂,难以维护
空间复杂度:O(1)
问题分析:
- 实现复杂:需要处理多种情况
- 难以理解:代码逻辑不清晰
- 难以扩展:无法处理变体问题
优化思考 - 第一步:动态规划
观察:问题具有最优子结构
关键问题:如何定义子问题?
定义:dp[i] = 和为 i 的完全平方数的最少数量
状态转移方程:
dp[i] = min(dp[i - j*j] + 1) for all j where j*j <= i
为什么这样思考?
- 如果我们选择了
j*j,那么问题变成i - j*j dp[i - j*j]是已知的子问题- 我们只需要枚举所有可能的
j
优化后的思路:
dp = [0] * (n + 1)
for i in range(1, n + 1):
dp[i] = min(dp[i - j*j] + 1 for j in range(1, int(i**0.5) + 1))
时间复杂度:O(n√n)
- 外层循环:O(n)
- 内层循环:O(√n)
- 总计:O(n) × O(√n) = O(n√n)
空间复杂度:O(n)
- dp 数组:O(n)
优化思考 - 第二步:数学优化(拉格朗日四平方定理)
定理:任何正整数都可以表示为最多 4 个完全平方数的和。
推论:
- 如果
n是完全平方数,返回 1 - 如果
n可以表示为两个完全平方数的和,返回 2 - 如果
n满足特定条件(勒让德三平方定理),返回 3 - 否则返回 4
为什么这样思考?
- 数学定理可以快速判断
- 避免动态规划的高复杂度
- 时间复杂度可以降到 O(√n)
优化思考 - 第三步:BFS 最短路径
观察:问题可以转化为图的最短路径问题
建模:
- 节点:数字
i - 边:从
i到i - j*j(如果j*j <= i) - 权重:每条边的权重都是 1
目标:从 n 到 0 的最短路径
为什么这样思考?
- BFS 天然适合求最短路径
- 可以提前终止(找到 0 就停止)
- 比动态规划更快(实际运行时间)
时间复杂度:O(√n)^h,h 是答案
- 最坏情况:h = 4
- 实际运行:比 O(n√n) 快很多
解题思路
核心思想
动态规划:将问题分解为子问题,从底向上求解。
为什么这样思考?
-
最优子结构:
n的最优解依赖于n - j*j的最优解- 子问题重叠,可以重复使用
-
无后效性:
dp[i]只依赖于比i小的值- 不依赖于计算路径
-
边界明确:
dp[0] = 0(0 个完全平方数)dp[1] = 1(1 = 1)
详细算法流程
步骤1:初始化 dp 数组
dp = [float('inf')] * (n + 1)
dp[0] = 0 # 边界条件
作用:
dp[i]表示和为i的完全平方数的最少数量- 初始化为无穷大,表示未计算
dp[0] = 0是递归的基础
步骤2:填表
for i in range(1, n + 1):
# 尝试所有可能的完全平方数 j*j
for j in range(1, int(i**0.5) + 1):
dp[i] = min(dp[i], dp[i - j*j] + 1)
关键点详解:
-
为什么外层循环从 1 到 n?
- 从小到大计算,确保
dp[i - j*j]已经计算过 i - j*j < i,所以已经计算过
- 从小到大计算,确保
-
为什么内层循环从 1 到 √i?
j*j必须小于等于i- 最大的
j是√i - 示例:
i = 12,j最大为 3(3² = 9 ≤ 12)
-
为什么是
dp[i - j*j] + 1?dp[i - j*j]是和为i - j*j的最少数量- 加上 1 表示再加上
j*j - 示例:
dp[12] = dp[12 - 4] + 1 = dp[8] + 1
示例:
n = 12
i=1: dp[1] = dp[1-1] + 1 = dp[0] + 1 = 1
i=2: dp[2] = dp[2-1] + 1 = dp[1] + 1 = 2
i=3: dp[3] = dp[3-1] + 1 = dp[2] + 1 = 3
i=4: dp[4] = dp[4-4] + 1 = dp[0] + 1 = 1
i=5: dp[5] = min(dp[5-1]+1, dp[5-4]+1) = min(dp[4]+1, dp[1]+1) = min(2, 2) = 2
...
i=12: dp[12] = min(dp[12-1]+1, dp[12-4]+1, dp[12-9]+1)
= min(dp[11]+1, dp[8]+1, dp[3]+1)
= min(3, 2, 4)
= 2
关键细节说明
细节1:为什么 dp[0] = 0?
dp[0] = 0 # 0 可以由 0 个完全平方数组成
原因:
- 0 是递归的终止条件
- 表示不需要任何完全平方数就能组成 0
- 类似于数学中的"空和为 0"
细节2:为什么初始化为 float('inf')?
dp = [float('inf')] * (n + 1)
dp[0] = 0
原因:
- 无穷大表示"未计算"或"不可达"
min(inf, x) = x,确保第一次更新正确- 避免使用 0 初始化导致
min(0, x)错误
细节3:为什么用 int(i**0.5) + 1?
for j in range(1, int(i**0.5) + 1):
# ...
原因:
int(i**0.5)是√i的整数部分+1是因为range是左闭右开区间- 示例:
i = 4,int(4**0.5) = 2,range(1, 3)→[1, 2]✓
细节4:为什么可以保证最优解?
dp[i] = min(dp[i - j*j] + 1 for all valid j)
原因:
- 枚举了所有可能的第一个完全平方数
j*j dp[i - j*j]已经是最优解(归纳假设)min确保选择了最优的组合- 动态规划的"最优子结构"性质
边界条件分析
边界1:n = 0
输入:n = 0
输出:0
解释:
dp[0] = 0
0 个完全平方数组成 0
边界2:n = 1
输入:n = 1
输出:1
过程:
dp[1] = dp[1-1] + 1 = dp[0] + 1 = 1
边界3:n 是完全平方数
输入:n = 9
输出:1
过程:
dp[9] = min(dp[9-1]+1, dp[9-4]+1, dp[9-9]+1)
= min(dp[8]+1, dp[5]+1, dp[0]+1)
= min(3, 3, 1)
= 1
边界4:n = 12
输入:n = 12
输出:3
过程:
dp[12] = min(dp[12-1]+1, dp[12-4]+1, dp[12-9]+1)
= min(dp[11]+1, dp[8]+1, dp[3]+1)
= min(4, 3, 4)
= 3
验证:12 = 4 + 4 + 4 ✓
复杂度分析(详细版)
时间复杂度:
- 外层循环:O(n),从 1 到 n
- 内层循环:O(√n),从 1 到 √i
- 总计:O(n) × O(√n) = O(n√n)
为什么是 O(n√n)?
- 对于每个 i,最多有 √i 个 j
- √i ≤ √n
- 总操作次数 ≈ n × √n = n√n
空间复杂度:
- dp 数组:O(n)
- 其他变量:O(1)
- 总计:O(n)
图解过程
n = 12
初始化:dp = [0, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf]
i=1: j=1
dp[1] = dp[1-1] + 1 = dp[0] + 1 = 1
dp = [0, 1, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf]
i=2: j=1
dp[2] = dp[2-1] + 1 = dp[1] + 1 = 2
dp = [0, 1, 2, inf, inf, inf, inf, inf, inf, inf, inf, inf, inf]
i=3: j=1
dp[3] = dp[3-1] + 1 = dp[2] + 1 = 3
dp = [0, 1, 2, 3, inf, inf, inf, inf, inf, inf, inf, inf, inf]
i=4: j=1,2
dp[4] = min(dp[4-1]+1, dp[4-4]+1) = min(dp[3]+1, dp[0]+1) = min(4, 1) = 1
dp = [0, 1, 2, 3, 1, inf, inf, inf, inf, inf, inf, inf, inf]
i=5: j=1,2
dp[5] = min(dp[5-1]+1, dp[5-4]+1) = min(dp[4]+1, dp[1]+1) = min(2, 2) = 2
dp = [0, 1, 2, 3, 1, 2, inf, inf, inf, inf, inf, inf, inf]
i=6: j=1,2
dp[6] = min(dp[6-1]+1, dp[6-4]+1) = min(dp[5]+1, dp[2]+1) = min(3, 3) = 3
dp = [0, 1, 2, 3, 1, 2, 3, inf, inf, inf, inf, inf, inf]
i=7: j=1,2
dp[7] = min(dp[7-1]+1, dp[7-4]+1) = min(dp[6]+1, dp[3]+1) = min(4, 4) = 4
dp = [0, 1, 2, 3, 1, 2, 3, 4, inf, inf, inf, inf, inf]
i=8: j=1,2
dp[8] = min(dp[8-1]+1, dp[8-4]+1) = min(dp[7]+1, dp[4]+1) = min(5, 2) = 2
dp = [0, 1, 2, 3, 1, 2, 3, 4, 2, inf, inf, inf, inf]
i=9: j=1,2,3
dp[9] = min(dp[9-1]+1, dp[9-4]+1, dp[9-9]+1) = min(dp[8]+1, dp[5]+1, dp[0]+1) = min(3, 3, 1) = 1
dp = [0, 1, 2, 3, 1, 2, 3, 4, 2, 1, inf, inf, inf]
i=10: j=1,2,3
dp[10] = min(dp[10-1]+1, dp[10-4]+1, dp[10-9]+1) = min(dp[9]+1, dp[6]+1, dp[1]+1) = min(2, 4, 2) = 2
dp = [0, 1, 2, 3, 1, 2, 3, 4, 2, 1, 2, inf, inf]
i=11: j=1,2,3
dp[11] = min(dp[11-1]+1, dp[11-4]+1, dp[11-9]+1) = min(dp[10]+1, dp[7]+1, dp[2]+1) = min(3, 5, 3) = 3
dp = [0, 1, 2, 3, 1, 2, 3, 4, 2, 1, 2, 3, inf]
i=12: j=1,2,3
dp[12] = min(dp[12-1]+1, dp[12-4]+1, dp[12-9]+1) = min(dp[11]+1, dp[8]+1, dp[3]+1) = min(4, 3, 4) = 3
dp = [0, 1, 2, 3, 1, 2, 3, 4, 2, 1, 2, 3, 3]
结果:dp[12] = 3
代码实现
func numSquares(n int) int {
dp := make([]int, n+1)
// 初始化为最大值
for i := range dp {
dp[i] = math.MaxInt32
}
dp[0] = 0
// 填表
for i := 1; i <= n; i++ {
for j := 1; j*j <= i; j++ {
if dp[i-j*j]+1 < dp[i] {
dp[i] = dp[i-j*j] + 1
}
}
}
return dp[n]
}
关键点:
dp[0] = 0是边界条件- 从小到大计算,确保子问题已解决
- 枚举所有可能的完全平方数
执行过程演示
输入:n = 12
初始化:dp = [0, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647, 2147483647]
i=1: j=1
dp[1] = min(2147483647, dp[0]+1) = 1
i=2: j=1
dp[2] = min(2147483647, dp[1]+1) = 2
i=3: j=1
dp[3] = min(2147483647, dp[2]+1) = 3
i=4: j=1,2
j=1: dp[4] = min(2147483647, dp[3]+1) = 4
j=2: dp[4] = min(4, dp[0]+1) = 1
i=5: j=1,2
j=1: dp[5] = min(2147483647, dp[4]+1) = 2
j=2: dp[5] = min(2, dp[1]+1) = 2
i=6: j=1,2
j=1: dp[6] = min(2147483647, dp[5]+1) = 3
j=2: dp[6] = min(3, dp[2]+1) = 3
i=7: j=1,2
j=1: dp[7] = min(2147483647, dp[6]+1) = 4
j=2: dp[7] = min(4, dp[3]+1) = 4
i=8: j=1,2
j=1: dp[8] = min(2147483647, dp[7]+1) = 5
j=2: dp[8] = min(5, dp[4]+1) = 2
i=9: j=1,2,3
j=1: dp[9] = min(2147483647, dp[8]+1) = 3
j=2: dp[9] = min(3, dp[5]+1) = 3
j=3: dp[9] = min(3, dp[0]+1) = 1
i=10: j=1,2,3
j=1: dp[10] = min(2147483647, dp[9]+1) = 2
j=2: dp[10] = min(2, dp[6]+1) = 2
j=3: dp[10] = min(2, dp[1]+1) = 2
i=11: j=1,2,3
j=1: dp[11] = min(2147483647, dp[10]+1) = 3
j=2: dp[11] = min(3, dp[7]+1) = 3
j=3: dp[11] = min(3, dp[2]+1) = 3
i=12: j=1,2,3
j=1: dp[12] = min(2147483647, dp[11]+1) = 4
j=2: dp[12] = min(4, dp[8]+1) = 3
j=3: dp[12] = min(3, dp[3]+1) = 3
结果:dp[12] = 3
常见错误
错误1:初始化为 0
❌ 错误代码:
dp := make([]int, n+1)
// dp 默认为 0
for i := 1; i <= n; i++ {
for j := 1; j*j <= i; j++ {
dp[i] = min(dp[i], dp[i-j*j]+1) // 错误!dp[i] 初始为 0
}
}
✅ 正确代码:
dp := make([]int, n+1)
for i := range dp {
dp[i] = math.MaxInt32 // 初始化为最大值
}
dp[0] = 0
原因:
dp[i]初始为 0,min(0, x)永远是 0- 导致结果错误
错误2:循环范围错误
❌ 错误代码:
for j := 1; j <= int(math.Sqrt(float64(n))); j++ {
// 错误!用了 n 而不是 i
}
✅ 正确代码:
for j := 1; j*j <= i; j++ {
// 正确!用 i
}
原因:
- 应该是
j*j <= i,不是j*j <= n - 用 n 会导致越界或计算错误
错误3:忘记边界条件
❌ 错误代码:
dp := make([]int, n+1)
// 忘记设置 dp[0] = 0
✅ 正确代码:
dp := make([]int, n+1)
dp[0] = 0 // 边界条件
原因:
dp[0] = 0是递归的基础- 没有它,
dp[i-j*j]无法正确计算
进阶问题
Q1: 如何优化空间复杂度?
思路:使用贪心算法或数学定理
// 基于拉格朗日四平方定理
func numSquaresOptimized(n int) int {
// 检查是否为完全平方数
if isPerfectSquare(n) {
return 1
}
// 检查是否可以表示为两个完全平方数的和
for i := 1; i*i <= n; i++ {
if isPerfectSquare(n - i*i) {
return 2
}
}
// 检查是否满足勒让德三平方定理
// n != 4^a * (8b + 7)
if n == 4*n {
return 4
}
return 3
}
func isPerfectSquare(n int) bool {
sqrt := int(math.Sqrt(float64(n)))
return sqrt*sqrt == n
}
时间复杂度:O(√n)
Q2: 如何返回具体的完全平方数组合?
func numSquaresWithSolution(n int) ([]int, int) {
dp := make([]int, n+1)
prev := make([]int, n+1) // 记录前一个状态
for i := range dp {
dp[i] = math.MaxInt32
}
dp[0] = 0
for i := 1; i <= n; i++ {
for j := 1; j*j <= i; j++ {
if dp[i-j*j]+1 < dp[i] {
dp[i] = dp[i-j*j] + 1
prev[i] = j * j // 记录选择的完全平方数
}
}
}
// 回溯构建解
solution := []int{}
curr := n
for curr > 0 {
square := prev[curr]
solution = append(solution, square)
curr -= square
}
return solution, dp[n]
}
P7 加分项
深度理解
- 最优子结构:问题的最优解包含子问题的最优解
- 重叠子问题:子问题会被重复计算,动态规划避免重复
- 数学定理:拉格朗日四平方定理可以优化算法
实战扩展
- 背包问题:完全背包的变种
- 最短路径:可以转化为 BFS 问题
- 业务场景:货币兑换、资源分配
变形题目
- 最少硬币数量(硬币面值不同)
- 完全平方数的所有组合
- 限制完全平方数的使用次数
总结
核心要点:
- 动态规划:从底向上,逐步求解
- 状态转移:
dp[i] = min(dp[i - j*j] + 1) - 边界条件:
dp[0] = 0
易错点:
- 初始化错误(0 vs MaxInt32)
- 循环范围错误(n vs i)
- 忘记边界条件
最优解法:动态规划,时间 O(n√n),空间 O(n)