This commit is contained in:
@@ -0,0 +1,651 @@
|
||||
# 十大经典动态规划题(Go · ACM 模式)
|
||||
|
||||
!!! note "💡 一句话概述"
|
||||
精选 10 道由易到难的 LeetCode 经典动态规划题,每道题重点讲解**状态定义、状态转移方程和 base case**,并给出完整的 Go **ACM 模式**(可编译运行的含 `main` 输入输出)题解。
|
||||
|
||||
---
|
||||
|
||||
## 🔑 核心概念
|
||||
|
||||
1. **状态(dp 含义)** — DP 的第一步:明确 `dp[i]` / `dp[i][j]` 代表什么,定义清楚才能写出转移方程
|
||||
2. **转移方程** — 用子问题递推当前问题,是 DP 的灵魂;从"最后一步"倒推最容易写对
|
||||
3. **base case(边界)** — 递推的起跑线,通常是 `i=0`、空串、对角线等最小子问题
|
||||
4. **遍历顺序** — 一维从左到右;二维按行;区间按长度;背包注意 0/1 逆序 vs 完全背包顺序
|
||||
5. **滚动数组 / 状态压缩** — 当转移只依赖最近几行时,可由二维压成一维,降低空间复杂度
|
||||
|
||||
---
|
||||
|
||||
## 📝 详细说明
|
||||
|
||||
### 动态规划通用思考框架
|
||||
|
||||
拿到一道 DP 题,按以下 4 步走(全文每题都按这 4 步展开):
|
||||
|
||||
1. **明确状态**:`dp[i]` 表示"以第 i 个元素结尾 / 前 i 个元素"的答案是什么
|
||||
2. **写出转移**:考虑**最后一个动作**,把问题拆成更小的子问题(如爬到第 i 阶最后一步是 1 阶还是 2 阶)
|
||||
3. **确定 base**:`dp[0]`、`dp[1]` 或空串、边界行/列的值
|
||||
4. **决定顺序**:确保计算 `dp[i]` 时依赖的子问题(`dp[i-1]` 等)已经算好
|
||||
|
||||
### 10 题难度梯度总览
|
||||
|
||||
| 难度 | 题号 | 题目 | DP 范式 | 核心转移方程 |
|
||||
|------|------|------|---------|--------------|
|
||||
| ★ | 1 | 爬楼梯 | 一维线性 | `dp[i]=dp[i-1]+dp[i-2]` |
|
||||
| ★ | 2 | 打家劫舍 | 一维 选/不选 | `dp[i]=max(dp[i-1],dp[i-2]+nums[i])` |
|
||||
| ★★ | 3 | 最长递增子序列 | 一维序列 | `dp[i]=max(dp[j]+1)`(`nums[j]<nums[i]`)|
|
||||
| ★★ | 4 | 不同路径 | 二维网格 | `dp[i][j]=dp[i-1][j]+dp[i][j-1]` |
|
||||
| ★★ | 5 | 最长公共子序列 | 二维序列 | 相同 `+1`,不同取 max 左右 |
|
||||
| ★★ | 6 | 分割等和子集 | 0/1 背包 | 逆序 `dp[j] \|= dp[j-num]` |
|
||||
| ★★★ | 7 | 完全平方数 | 完全背包 | 顺序 `dp[j]=min(dp[j],dp[j-s]+1)` |
|
||||
| ★★★ | 8 | 单词拆分 | 完全背包/匹配 | `dp[i]` 由 `dp[j]`+子串命中推出 |
|
||||
| ★★★ | 9 | 编辑距离 | 二维序列 三操作 | 增删替三者取 min |
|
||||
| ★★★ | 10 | 最长回文子序列 | 区间 DP | `dp[i][j]=dp[i+1][j-1]+2` |
|
||||
|
||||
---
|
||||
|
||||
## 第 1 题:爬楼梯
|
||||
|
||||
!!! note "题目"
|
||||
假设你正在爬楼梯。需要 **n** 阶你才能到楼顶。每次你可以爬 **1** 或 **2** 个台阶。问有多少种不同方法可以爬到楼顶?(LeetCode 70)
|
||||
|
||||
### 状态定义与转移
|
||||
|
||||
- 状态:`f[i]` 表示爬到第 `i` 阶的方法总数
|
||||
- 转移:爬到第 `i` 阶,最后一步要么从 `i-1` 走 1 阶,要么从 `i-2` 走 2 阶,因此 **`f[i] = f[i-1] + f[i-2]`**
|
||||
- base:`f[0]=1`(原地),`f[1]=1`
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["f[i-2]"] --> C["f[i] = f[i-1] + f[i-2]"]
|
||||
B["f[i-1]"] --> C
|
||||
```
|
||||
|
||||
### ACM 题解(Go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
var n int
|
||||
fmt.Scan(&n) // ACM 模式:读入 n
|
||||
if n <= 2 {
|
||||
fmt.Println(n)
|
||||
return
|
||||
}
|
||||
f := make([]int, n+1)
|
||||
f[1], f[2] = 1, 2
|
||||
for i := 3; i <= n; i++ {
|
||||
f[i] = f[i-1] + f[i-2]
|
||||
}
|
||||
fmt.Println(f[n])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 2 题:打家劫舍
|
||||
|
||||
!!! note "题目"
|
||||
你是一个专业小偷,沿街的房屋排成一排,相邻房屋连着保安。给定每个房屋的金额 `nums[i]`,求**不偷相邻两家**的前提下能偷到的最大金额。(LeetCode 198)
|
||||
|
||||
### 状态定义与 DP
|
||||
|
||||
- 状态:`dp[i]` 表示偷到前 **i** 间房子(下标 0..i)能得到的最大金额
|
||||
- 转移:对第 `i` 间房,**偷**(`nums[i]` + 前 `i-2` 间最优)还是**不偷**(前 `i-1` 间最优),取大者:
|
||||
**`dp[i] = max(dp[i-1], dp[i-2] + nums[i])`**
|
||||
- base:`dp[0]=nums[0]`,`dp[1]=max(nums[0],nums[1])`
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
A["dp[i-1] 不偷"] -.-> D{"max"}
|
||||
B["dp[i-2] + nums[i] 偷"] --> D
|
||||
D --> E["dp[i]"]
|
||||
```
|
||||
|
||||
### ACM 题解(Go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func main() {
|
||||
var n int
|
||||
fmt.Scan(&n)
|
||||
nums := make([]int, n)
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Split(bufio.ScanWords)
|
||||
for i := 0; i < n; i++ {
|
||||
sc.Scan()
|
||||
fmt.Sscan(sc.Text(), &nums[i])
|
||||
}
|
||||
if n == 0 {
|
||||
fmt.Println(0)
|
||||
return
|
||||
}
|
||||
if n == 1 {
|
||||
fmt.Println(nums[0])
|
||||
return
|
||||
}
|
||||
dp := make([]int, n)
|
||||
dp[0], dp[1] = nums[0], max(nums[0], nums[1])
|
||||
for i := 2; i < n; i++ {
|
||||
dp[i] = max(dp[i-1], dp[i-2]+nums[i])
|
||||
}
|
||||
fmt.Println(dp[n-1])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 3 题:最长递增子序列(LIS)
|
||||
|
||||
!!! note "题目说明"
|
||||
给整数数组 `nums`,求**最长严格递增**的子序列(子序列可不连续,但要保持原顺序)的长度。(LeetCode 300)
|
||||
|
||||
### 思路与转移
|
||||
|
||||
- 状态:`dp[i]` 表示**以 `nums[i]` 结尾**的最长递增子序列长度
|
||||
- 转移:从左往右枚举 `j < i`,若 `nums[j] < nums[i]`,则 `nums[i]` 可接到以 `nums[j]` 结尾的子序列后面,故 **dp[i] = max(dp[i], dp[j] + 1)**
|
||||
- base:每个元素单独成序列,`dp[i]` 初始为 1
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["i-th 元素"] --> B{"有 j<i 且 nums[j]<nums[i] ?"}
|
||||
B -->|是| C["dp[i] = max(dp[i], dp[j]+1)"]
|
||||
B -->|否| D["dp[i] = 1"]
|
||||
```
|
||||
|
||||
### ACM 题解(Go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var n int
|
||||
fmt.Scan(&n)
|
||||
nums := make([]int, n)
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Split(bufio.ScanWords)
|
||||
for i := 0; i < n; i++ {
|
||||
sc.Scan()
|
||||
fmt.Sscan(sc.Text(), &nums[i])
|
||||
}
|
||||
dp := make([]int, n)
|
||||
ans := 0
|
||||
for i := 0; i < n; i++ {
|
||||
dp[i] = 1
|
||||
for j := 0; j < i; j++ {
|
||||
if nums[j] < nums[i] && dp[j]+1 > dp[i] {
|
||||
dp[i] = dp[j] + 1
|
||||
}
|
||||
}
|
||||
if dp[i] > ans {
|
||||
ans = dp[i]
|
||||
}
|
||||
}
|
||||
fmt.Println(ans)
|
||||
}
|
||||
```
|
||||
|
||||
!!! tip "O(n log n) 优化"
|
||||
用贪心+二分维护"长度为 len 的最小末尾",可将 LIS 优化到 O(n log n),但上面的 O(n²) 已能直观体现 dp 精髓,适合入门。
|
||||
|
||||
---
|
||||
|
||||
## 第 4 题:不同路径
|
||||
|
||||
!!! note "题目说明"
|
||||
机器人在 `m × n` 网格左上角,每次只能**向右或向下**走一格,问到达右下角有多少条不同路径?(LeetCode 62)
|
||||
|
||||
### 状态定义与转移
|
||||
|
||||
- 状态:`dp[i][j]` 表示从左上角走到 `(i,j)` 的路径条数
|
||||
- 转移:到达 `(i,j)` 只能从左边的 `(i,j-1)` 或上边的 `(i-1,j)` 过来,故 **dp[i][j] = dp[i-1][j] + dp[i][j-1]**
|
||||
- base:第一行只能一路向右、第一列只能一路向下,故均为 1
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A["(i-1,j) 从上"] --> B["dp[i][j]"]
|
||||
C["(i,j-1) 从左"] --> B
|
||||
B --> D["dp[i][j] = dp[i-1][j] + dp[i][j-1]"]
|
||||
```
|
||||
|
||||
### ACM 题解(Go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
var m, n int
|
||||
fmt.Scan(&m, &n)
|
||||
dp := make([][]int, m)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n)
|
||||
dp[i][0] = 1
|
||||
}
|
||||
for j := 0; j < n; j++ {
|
||||
dp[0][j] = 1
|
||||
}
|
||||
for i := 1; i < m; i++ {
|
||||
for j := 1; j < n; j++ {
|
||||
dp[i][j] = dp[i-1][j] + dp[i][j-1]
|
||||
}
|
||||
}
|
||||
fmt.Println(dp[m-1][n-1])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 5 题:最长公共子序列(LCS)
|
||||
|
||||
!!! note "题目说明"
|
||||
给定两个字符串 `text1` 和 `text2`,返回它们的最长公共子序列的长度。子序列可以不连续。(LeetCode 1143)
|
||||
|
||||
### 二维表转移
|
||||
|
||||
- 状态:`dp[i][j]` 表示 `text1[0..i-1]` 与 `text2[0..j-1]` 的最长公共子序列长度
|
||||
- 转移:
|
||||
- 若 `text1[i-1] == text2[j-1]`:可拼在当前匹配字符,**dp[i][j] = dp[i-1][j-1] + 1**
|
||||
- 否则取各自少一个字符中的较大者:**dp[i][j] = max(dp[i-1][j], dp[i][j-1])**
|
||||
- base:`dp[0][*]=dp[*][0]=0`(空串与任何串的 LCS 为 0)
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
A{"text1[i-1] == text2[j-1] ?"}
|
||||
A -->|==| B["dp[i][j] = dp[i-1][j-1] + 1"]
|
||||
A -->|!=| C["dp[i][j] = max(dp[i-1][j], dp[i][j-1])"]
|
||||
```
|
||||
|
||||
### ACM 题解(Go)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func main() {
|
||||
var a, b string
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Split(bufio.ScanWords)
|
||||
sc.Scan()
|
||||
a = sc.Text()
|
||||
sc.Scan()
|
||||
b = sc.Text()
|
||||
m, n := len(a), len(b)
|
||||
dp := make([][]int, m+1)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n+1)
|
||||
}
|
||||
for i := 1; i <= m; i++ {
|
||||
for j := 1; j <= n; j++ {
|
||||
if a[i-1] == b[j-1] {
|
||||
dp[i][j] = dp[i-1][j-1] + 1
|
||||
} else {
|
||||
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println(dp[m][n])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 6 题:分割等和子集
|
||||
|
||||
!!! note "题目条件"
|
||||
给定**非空**正整数数组 `nums`,判断能否把它分割成**两个元素和相等的子集**,即能否选出若干个数之和等于总和的一半。(LeetCode 416)
|
||||
|
||||
### 转化为 0/1 背包
|
||||
|
||||
sum 为奇直接不可能。目标 `target = sum/2`。这就是一个容量为 `target` 的 **0/1 背包问题**:每个数只能取一次,问能否恰好凑出 `target`。
|
||||
|
||||
- 状态:`dp[j]` 表示能否选出若干个数之和恰好等于 `j`
|
||||
- 转移:对每个 `num`,若 `dp[j-num]==true` 则 `dp[j]` 可为 true,故 **dp[j] = dp[j] || dp[j - num]**
|
||||
- 关键:背包**逆序遍历** `j`,保证 `num` 不会被重复使用(0/1 背包特性)
|
||||
- base:`dp[0]=true`
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var n int
|
||||
fmt.Scan(&n)
|
||||
nums := make([]int, n)
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Split(bufio.ScanWords)
|
||||
sum := 0
|
||||
for i := 0; i < n; i++ {
|
||||
sc.Scan()
|
||||
fmt.Sscan(sc.Text(), &nums[i])
|
||||
sum += nums[i]
|
||||
}
|
||||
if sum%2 == 1 {
|
||||
fmt.Println("NO")
|
||||
return
|
||||
}
|
||||
target := sum / 2
|
||||
dp := make([]bool, target+1)
|
||||
dp[0] = true
|
||||
for _, num := range nums {
|
||||
for j := target; j >= num; j-- { // 必须逆序:0/1 背包,一个数只能用一次
|
||||
if dp[j-num] {
|
||||
dp[j] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
if dp[target] {
|
||||
fmt.Println("YES")
|
||||
} else {
|
||||
fmt.Println("NO")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 7 题:完全平方数
|
||||
|
||||
**题目描述**:给定正整数 `n`,求**最少数量的完全平方数**(如 1,4,9,16...)使其和为 `n`。(LeetCode 279)
|
||||
|
||||
这本质是一个**完全背包**(每种平方数可以重复取)求最小个数:
|
||||
|
||||
- 状态:`dp[j]` 表示凑出 `j` 所需的最少完全平方数个数
|
||||
- 转移(加入一个平方数 `s`):**dp[j] = min(dp[j], dp[j-s] + 1)**
|
||||
- 注意这是**完全背包**:`j` **顺序**遍历,允许重复取同一个平方数
|
||||
- base:`dp[0]=0`,其余初始化为无穷大
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import "fmt"
|
||||
|
||||
func main() {
|
||||
var n int
|
||||
fmt.Scan(&n)
|
||||
const INF = 1 << 30
|
||||
dp := make([]int, n+1)
|
||||
for i := range dp {
|
||||
dp[i] = INF
|
||||
}
|
||||
dp[0] = 0
|
||||
for s := 1; s*s <= n; s++ { // 物品:完全平方数 s^2
|
||||
sq := s * s
|
||||
for j := sq; j <= n; j++ { // 顺序:完全背包,可重复取
|
||||
if dp[j-sq]+1 < dp[j] {
|
||||
dp[j] = dp[j-sq] + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println(dp[n])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 8 题:单词拆分
|
||||
|
||||
**题目描述**:给定字符串 `s` 和一个字典 `wordDict`,判断 `s` 是否可以被空格拆分成字典中一个或多个单词。(LeetCode 139)
|
||||
|
||||
- 状态:`dp[i]` 表示 `s` 的前 `i` 个字符能否被成功拆分
|
||||
- 转移:若存在 `j < i` 使得 `dp[j]==true` 且区间 `s[j:i]` 是一个字典单词,则 `dp[i]=true`
|
||||
即 **dp[i] = dp[j] && s[j:i] ∈ wordDict**(只要任一个 j 成立即可)
|
||||
- base:`dp[0]=true`(空串可拆分)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
var n int
|
||||
var s string
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Split(bufio.ScanWords)
|
||||
sc.Scan()
|
||||
s = sc.Text()
|
||||
sc.Scan()
|
||||
fmt.Sscan(sc.Text(), &n)
|
||||
words := make(map[string]bool)
|
||||
for i := 0; i < n; i++ {
|
||||
sc.Scan()
|
||||
words[sc.Text()] = true
|
||||
}
|
||||
m := len(s)
|
||||
dp := make([]bool, m+1)
|
||||
dp[0] = true
|
||||
for i := 1; i <= m; i++ {
|
||||
for j := 0; j < i; j++ {
|
||||
if dp[j] && words[s[j:i]] {
|
||||
dp[i] = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if dp[m] {
|
||||
fmt.Println("YES")
|
||||
} else {
|
||||
fmt.Println("NO")
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 9 题:编辑距离
|
||||
|
||||
**题目描述**:给定两个字符串 `word1` 和 `word2`,通过对 `word1` 进行以下三种操作——插入、删除、替换一个字符——求把 `word1` 变成 `word2` 的**最少操作数**。(LeetCode 72)
|
||||
|
||||
- 状态:`dp[i][j]` 表示把 `word1[0:i]` 变成 `word2[0:j]` 的最少步数
|
||||
- 转移(看 `word1[i-1]` 与 `word2[j-1]`):
|
||||
- 相同:**dp[i][j] = dp[i-1][j-1]**(不用动)
|
||||
- 不同:取三者的最小值加 1
|
||||
- 删除 word1[i-1]:`dp[i-1][j] + 1`
|
||||
- 插入 word2[j-1]:`dp[i][j-1] + 1`
|
||||
- 替换:`dp[i-1][j-1] + 1`
|
||||
- base:`dp[i][0]=i`(全删),`dp[0][j]=j`(全插)
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func min(a, b int) int {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func main() {
|
||||
var w1, w2 string
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Split(bufio.ScanWords)
|
||||
sc.Scan()
|
||||
w1 = sc.Text()
|
||||
sc.Scan()
|
||||
w2 = sc.Text()
|
||||
m, n := len(w1), len(w2)
|
||||
dp := make([][]int, m+1)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n+1)
|
||||
}
|
||||
for i := 0; i <= m; i++ {
|
||||
dp[i][0] = i
|
||||
}
|
||||
for j := 0; j <= n; j++ {
|
||||
dp[0][j] = j
|
||||
}
|
||||
for i := 1; i <= m; i++ {
|
||||
for j := 1; j <= n; j++ {
|
||||
c := dp[i-1][j-1] // 替换的代价(若字符相等则 0)
|
||||
if w1[i-1] == w2[j-1] {
|
||||
dp[i][j] = dp[i-1][j-1]
|
||||
continue
|
||||
}
|
||||
dp[i][j] = min(min(dp[i-1][j]+1, dp[i][j-1]+1), c+1)
|
||||
}
|
||||
}
|
||||
fmt.Println(dp[m][n])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第 10 题:最长回文子序列
|
||||
|
||||
**题目描述**:给定字符串 `s`,找到其中最长的回文子序列的长度(子序列可不连续)。例如 `bbbab` → `bbbb` 长度为 4。(LeetCode 516)
|
||||
|
||||
- 状态:`dp[i][j]` 表示子串 `s[i:j+1]` 内的最长回文子序列长度
|
||||
- 转移(两端字符比较):
|
||||
- 若 `s[i]==s[j]`:**dp[i][j] = dp[i+1][j-1] + 2**(两端都能加进回文)
|
||||
- 若不同:**dp[i][j] = max(dp[i+1][j], dp[i][j-1])**(只能从两边去掉一个端)
|
||||
- base:`dp[i][i]=1`(单个字符是回文)
|
||||
- **遍历顺序**:区间 DP 要**按区间长度从小到大**计算,保证 `dp[i+1][j-1]` 等更短区间已求出
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func max(a, b int) int {
|
||||
if a > b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func main() {
|
||||
var s string
|
||||
sc := bufio.NewScanner(os.Stdin)
|
||||
sc.Split(bufio.ScanWords)
|
||||
sc.Scan()
|
||||
s = sc.Text()
|
||||
n := len(s)
|
||||
dp := make([][]int, n)
|
||||
for i := range dp {
|
||||
dp[i] = make([]int, n)
|
||||
dp[i][i] = 1
|
||||
}
|
||||
for length := 2; length <= n; length++ { // 按区间长度从小到大
|
||||
for i := 0; i+length-1 < n; i++ {
|
||||
j := i + length - 1
|
||||
if s[i] == s[j] {
|
||||
if i+1 <= j-1 {
|
||||
dp[i][j] = dp[i+1][j-1] + 2
|
||||
} else {
|
||||
dp[i][j] = 2
|
||||
}
|
||||
} else {
|
||||
dp[i][j] = max(dp[i+1][j], dp[i][j-1])
|
||||
}
|
||||
}
|
||||
}
|
||||
fmt.Println(dp[0][n-1])
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ 常见陷阱
|
||||
|
||||
!!! warning "0/1 背包 vs 完全背包的遍历顺序"
|
||||
分割等和子集(每个数只能用一次)必须**逆序**遍历 `j`;完全平方数(每个数可重复)必须**顺序**遍历 `j`。弄反了会导致重复计数或漏算,结果完全错误。
|
||||
|
||||
!!! warning "区间 DP 的遍历方向"
|
||||
第 10 题 `dp[i][j]` 依赖更短的 `dp[i+1][j-1]`,所以必须**按区间长度 level** 从小到大枚举,而不是简单地 i 从 0 到 n。
|
||||
|
||||
!!! warning "LIS 子序列 vs 子数组"
|
||||
dp[i] 定义成"以 nums[i] 结尾",答案应是所有 dp[i] 取 max,而不是直接 dp[n-1]。求最长递增**子数组**(连续)时才看 dp[n-1]。
|
||||
|
||||
!!! warning "编辑距离的相等的 case"
|
||||
当 `word1[i-1]==word2[j-1]` 时直接继承 `dp[i-1][j-1]`,不要额外 +1,否则会把"本来不用改"的情况误算成替换。
|
||||
|
||||
---
|
||||
|
||||
## 🏋️ 练习题
|
||||
|
||||
??? question "练习 1:LeetCode 746 — 最小花费爬楼梯"
|
||||
数组 `cost` 表示每阶楼梯往上爬的开销,可以从第 0 或第 1 阶开始,每步能走 1 或 2 阶,到顶(越过数组末尾)最少花费多少?请用 dp 写出转移方程。
|
||||
|
||||
??? success "答案"
|
||||
状态 `dp[i]` 表示到达第 i 阶的最小花费,因为可以从 `i-1`(花 `cost[i-1]`)或 `i-2`(花 `cost[i-2]`)到达,因此 `dp[i] = min(dp[i-1]+cost[i-1], dp[i-2]+cost[i-2])`,最终取 `min(dp[n], dp[n-1])`。
|
||||
|
||||
??? question "练习 2:LeetCode 322 — 零钱兑换"
|
||||
给定不同面额硬币 `coins` 和总金额 `amount`,问凑成 `amount` 至少需要多少枚硬币。它和第 7 题完全平方数有什么关系?可以仿照完全背包写出吗?
|
||||
|
||||
??? success "答案"
|
||||
类似,`dp[j] = min(dp[j], dp[j-coin]+1)`,且 `coin` 可重复使用,因此顺序遍历。与第 7 题唯一区别是物品从"平方数"换成"硬币面额",解法完全一致。
|
||||
|
||||
??? question "练习 3:LeetCode 5 — 最长回文子串(子串 vs 子序列)"
|
||||
第 10 题求的是**子序列**。如果改成**连续的子串**,状态和转移哪里有区别?请说明并改 dp。
|
||||
|
||||
??? success "思路"
|
||||
子串要求连续,`dp[i][j]` 表示 `s[i:j+1]` 是否回文,转移为:`s[i]==s[j]` 且 `j-i<=1` 或 `dp[i+1][j-1]`。需要**记住最长长度**的起点和结束。子串具有"连续性",子序列则可跳跃,这是关键差别。
|
||||
|
||||
---
|
||||
|
||||
## 🔗 相关链接
|
||||
|
||||
- [LeetCode 70. 爬楼梯](https://leetcode.cn/problems/climbing-stairs/) — 一维斐波那契入门
|
||||
- [LeetCode 198. 打家劫舍](https://leetcode.cn/problems/house-robber/) — 一维选/不选
|
||||
- [LeetCode 300. 最长递增子序列](https://leetcode.cn/problems/longest-increasing-subsequence/) — 序列 DP
|
||||
- [LeetCode 62. 不同路径](https://leetcode.cn/problems/unique-paths/) — 网格 DP
|
||||
- [LeetCode 1143. 最长公共子序列](https://leetcode.cn/problems/longest-common-subsequence/) — LCS
|
||||
- [LeetCode 416. 分割等和子集](https://leetcode.cn/problems/partition-equal-subset-sum/) — 0/1 背包
|
||||
- [LeetCode 279. 完全平方数](https://leetcode.cn/problems/perfect-squares/) — 完全背包
|
||||
- [LeetCode 139. 单词拆分](https://leetcode.cn/problems/word-break/) — 背包式匹配
|
||||
- [LeetCode 72. 编辑距离](https://leetcode.cn/problems/edit-distance/) — 三操作二维
|
||||
- [LeetCode 516. 最长回文子序列](https://leetcode.cn/problems/longest-palindromic-subsequence/) — 区间 DP
|
||||
@@ -1,3 +1,11 @@
|
||||
# 算法
|
||||
|
||||
算法相关笔记与文档。
|
||||
|
||||
## 主题
|
||||
|
||||
- 布隆过滤器
|
||||
- HeavyKeeper
|
||||
- 二叉树的遍历
|
||||
- 队列
|
||||
- 动态规划(十大经典题 · Go ACM 模式)
|
||||
@@ -122,6 +122,7 @@ nav:
|
||||
- HeavyKeeper: algorithm/heavykeeper.md
|
||||
- 二叉树的遍历: algorithm/binary-tree-traversal.md
|
||||
- 队列: algorithm/queue.md
|
||||
- 十大动态规划题: algorithm/dynamic-programming-top10.md
|
||||
- JVM:
|
||||
- jvm/index.md
|
||||
- 垃圾回收基础: jvm/garbage-collection.md
|
||||
|
||||
Reference in New Issue
Block a user