Files
interview/16-LeetCode Hot 100/完全平方数.md
yasinshaw 5c1c974e88 docs: 改进LeetCode二叉树题目解题思路
按照改进方案,为以下6个二叉树题目增强了解题思路的详细程度:

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

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

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

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

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

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

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2026-03-08 21:33:57 +08:00

663 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 完全平方数 (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
```
## 思路推导
### 暴力解法分析
**最直观的思路**:尝试所有可能的完全平方数组合。
```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) 快很多
## 解题思路
### 核心思想
**动态规划**:将问题分解为子问题,从底向上求解。
**为什么这样思考?**
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] {
dp[i] = dp[i-j*j] + 1
}
}
}
return dp[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)