docs: 改进LeetCode二叉树题目解题思路

按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

1. 二叉树的中序遍历
   - 增加"思路推导"部分,解释递归到迭代的转换
   - 详细说明迭代法的每个步骤
   - 增加执行过程演示和多种解法

2. 二叉树的最大深度
   - 增加"思路推导",对比DFS和BFS
   - 详细解释递归的基准情况
   - 增加多种解法和变体问题

3. 从前序与中序遍历序列构造二叉树
   - 详细解释前序和中序的特点
   - 增加"思路推导",说明如何分治
   - 详细说明切片边界计算

4. 对称二叉树
   - 解释镜像对称的定义
   - 详细说明递归比较的逻辑
   - 增加迭代解法和变体问题

5. 翻转二叉树
   - 解释翻转的定义和过程
   - 详细说明多值赋值的执行顺序
   - 增加多种解法和有趣的故事

6. 路径总和
   - 详细解释路径和叶子节点的定义
   - 说明为什么使用递减而非累加
   - 增加多种解法和变体问题

每个文件都包含:
- 完整的示例和边界条件分析
- 详细的算法流程和图解
- 关键细节说明
- 常见错误分析
- 复杂度分析(详细版)
- 执行过程演示
- 多种解法
- 变体问题
- 总结

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-03-08 21:33:57 +08:00
parent 67189941d8
commit 5c1c974e88
14 changed files with 7817 additions and 139 deletions

View File

@@ -1,25 +1,404 @@
# 完全平方数 (Perfect Squares)
LeetCode 279. Medium
## 题目描述
给你一个整数 n,返回和为 n 的完全平方数的最少数量。
给你一个整数 `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
```
## 思路推导
### 暴力解法分析
**最直观的思路**:尝试所有可能的完全平方数组合。
```python
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)
**问题分析**
1. 实现复杂:需要处理多种情况
2. 难以理解:代码逻辑不清晰
3. 难以扩展:无法处理变体问题
### 优化思考 - 第一步:动态规划
**观察**:问题具有最优子结构
**关键问题**:如何定义子问题?
**定义**`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`
**优化后的思路**
```python
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 个完全平方数的和。
**推论**
1. 如果 `n` 是完全平方数,返回 1
2. 如果 `n` 可以表示为两个完全平方数的和,返回 2
3. 如果 `n` 满足特定条件(勒让德三平方定理),返回 3
4. 否则返回 4
**为什么这样思考?**
- 数学定理可以快速判断
- 避免动态规划的高复杂度
- 时间复杂度可以降到 O(√n)
### 优化思考 - 第三步BFS 最短路径
**观察**:问题可以转化为图的最短路径问题
**建模**
- 节点:数字 `i`
- 边:从 `i``i - j*j`(如果 `j*j <= i`
- 权重:每条边的权重都是 1
**目标**:从 `n``0` 的最短路径
**为什么这样思考?**
- BFS 天然适合求最短路径
- 可以提前终止(找到 0 就停止)
- 比动态规划更快(实际运行时间)
**时间复杂度**O(√n)^hh 是答案
- 最坏情况h = 4
- 实际运行:比 O(n√n) 快很多
## 解题思路
### 动态规划
### 核心思想
dp[i] = min(dp[i - j*j] + 1) for all j where j*j <= i
**动态规划**:将问题分解为子问题,从底向上求解。
## Go 代码
**为什么这样思考?**
1. **最优子结构**
- `n` 的最优解依赖于 `n - j*j` 的最优解
- 子问题重叠,可以重复使用
2. **无后效性**
- `dp[i]` 只依赖于比 `i` 小的值
- 不依赖于计算路径
3. **边界明确**
- `dp[0] = 0`0 个完全平方数)
- `dp[1] = 1`1 = 1
### 详细算法流程
**步骤1初始化 dp 数组**
```python
dp = [float('inf')] * (n + 1)
dp[0] = 0 # 边界条件
```
**作用**
- `dp[i]` 表示和为 `i` 的完全平方数的最少数量
- 初始化为无穷大,表示未计算
- `dp[0] = 0` 是递归的基础
**步骤2填表**
```python
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. **为什么外层循环从 1 到 n**
- 从小到大计算,确保 `dp[i - j*j]` 已经计算过
- `i - j*j < i`,所以已经计算过
2. **为什么内层循环从 1 到 √i**
- `j*j` 必须小于等于 `i`
- 最大的 `j``√i`
- 示例:`i = 12``j` 最大为 33² = 9 ≤ 12
3. **为什么是 `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`**
```python
dp[0] = 0 # 0 可以由 0 个完全平方数组成
```
**原因**
- 0 是递归的终止条件
- 表示不需要任何完全平方数就能组成 0
- 类似于数学中的"空和为 0"
**细节2为什么初始化为 `float('inf')`**
```python
dp = [float('inf')] * (n + 1)
dp[0] = 0
```
**原因**
- 无穷大表示"未计算"或"不可达"
- `min(inf, x) = x`,确保第一次更新正确
- 避免使用 0 初始化导致 `min(0, x)` 错误
**细节3为什么用 `int(i**0.5) + 1`**
```python
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为什么可以保证最优解**
```python
dp[i] = min(dp[i - j*j] + 1 for all valid j)
```
**原因**
- 枚举了所有可能的第一个完全平方数 `j*j`
- `dp[i - j*j]` 已经是最优解(归纳假设)
- `min` 确保选择了最优的组合
- 动态规划的"最优子结构"性质
### 边界条件分析
**边界1n = 0**
```
输入n = 0
输出0
解释:
dp[0] = 0
0 个完全平方数组成 0
```
**边界2n = 1**
```
输入n = 1
输出1
过程:
dp[1] = dp[1-1] + 1 = dp[0] + 1 = 1
```
**边界3n 是完全平方数**
```
输入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
```
**边界4n = 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
```
---
## 代码实现
```go
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] {
@@ -27,9 +406,257 @@ func numSquares(n int) int {
}
}
}
return dp[n]
}
```
**复杂度:** O(n√n) 时间O(n) 空间
**关键点**
1. `dp[0] = 0` 是边界条件
2. 从小到大计算,确保子问题已解决
3. 枚举所有可能的完全平方数
---
## 执行过程演示
**输入**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
**错误代码**
```go
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
}
}
```
**正确代码**
```go
dp := make([]int, n+1)
for i := range dp {
dp[i] = math.MaxInt32 // 初始化为最大值
}
dp[0] = 0
```
**原因**
- `dp[i]` 初始为 0`min(0, x)` 永远是 0
- 导致结果错误
---
### 错误2循环范围错误
**错误代码**
```go
for j := 1; j <= int(math.Sqrt(float64(n))); j++ {
// 错误!用了 n 而不是 i
}
```
**正确代码**
```go
for j := 1; j*j <= i; j++ {
// 正确!用 i
}
```
**原因**
- 应该是 `j*j <= i`,不是 `j*j <= n`
- 用 n 会导致越界或计算错误
---
### 错误3忘记边界条件
**错误代码**
```go
dp := make([]int, n+1)
// 忘记设置 dp[0] = 0
```
**正确代码**
```go
dp := make([]int, n+1)
dp[0] = 0 // 边界条件
```
**原因**
- `dp[0] = 0` 是递归的基础
- 没有它,`dp[i-j*j]` 无法正确计算
---
## 进阶问题
### Q1: 如何优化空间复杂度?
**思路**:使用贪心算法或数学定理
```go
// 基于拉格朗日四平方定理
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: 如何返回具体的完全平方数组合?
```go
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 问题
- **业务场景**:货币兑换、资源分配
### 变形题目
1. 最少硬币数量(硬币面值不同)
2. 完全平方数的所有组合
3. 限制完全平方数的使用次数
---
## 总结
**核心要点**
1. **动态规划**:从底向上,逐步求解
2. **状态转移**`dp[i] = min(dp[i - j*j] + 1)`
3. **边界条件**`dp[0] = 0`
**易错点**
- 初始化错误0 vs MaxInt32
- 循环范围错误n vs i
- 忘记边界条件
**最优解法**:动态规划,时间 O(n√n),空间 O(n)