07-动态规划基础
💡 核心结论
动态规划本质
定义:将复杂问题分解为重叠子问题,存储子问题解避免重复计算
核心:最优子结构 + 重叠子问题 = 动态规划
vs递归:递归重复计算,DP记忆化存储
vs贪心:贪心局部最优,DP考虑所有可能
关键:找状态、写方程、定边界、优化空间
DP三要素(必须明确)
状态定义:dp[i]表示什么?
状态转移方程:dp[i]如何由之前的状态得到?
初始状态:dp[0]或dp[i][0]是什么?
解题步骤
暴力递归:写出递归解法
记忆化:加memo避免重复计算
自底向上:改为DP数组
空间优化:滚动数组或变量
DP vs 其他
方法 |
适用 |
时间 |
空间 |
|---|---|---|---|
暴力 |
数据小 |
指数级 |
O(1) |
贪心 |
局部最优=全局 |
O(n) |
O(1) |
DP |
重叠子问题 |
O(n)~O(n³) |
O(n)~O(n²) |
🎯 经典问题
1. 斐波那契数列
递归(指数时间)
def fib(n):
if n <= 1: return n
return fib(n-1) + fib(n-2)
# 时间:O(2^n) 重复计算
记忆化递归
def fib(n, memo={}):
if n in memo: return memo[n]
if n <= 1: return n
memo[n] = fib(n-1, memo) + fib(n-2, memo)
return memo[n]
# 时间:O(n) 空间:O(n)
DP数组
def fib(n):
if n <= 1: return n
dp = [0] * (n + 1)
dp[1] = 1
for i in range(2, n + 1):
dp[i] = dp[i-1] + dp[i-2]
return dp[n]
# 时间:O(n) 空间:O(n)
空间优化
def fib(n):
if n <= 1: return n
prev, curr = 0, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
# 时间:O(n) 空间:O(1)
2. 爬楼梯
一次爬1或2级,n级有多少种方法?
状态:dp[i] = 到第i级的方法数
转移:dp[i] = dp[i-1] + dp[i-2]
初始:dp[0]=1, dp[1]=1
def climbStairs(n):
if n <= 1: return 1
prev, curr = 1, 1
for _ in range(2, n + 1):
prev, curr = curr, prev + curr
return curr
3. 打家劫舍
不能偷相邻房屋,求最大金额
状态:dp[i] = 前i个房屋能偷的最大金额
转移:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
不偷第i个 偷第i个
def rob(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
prev, curr = 0, 0
for num in nums:
prev, curr = curr, max(curr, prev + num)
return curr
4. 最大子数组和(Kadane算法)
状态:dp[i] = 以nums[i]结尾的最大子数组和
转移:dp[i] = max(nums[i], dp[i-1] + nums[i])
def maxSubArray(nums):
max_sum = nums[0]
curr_sum = nums[0]
for i in range(1, len(nums)):
curr_sum = max(nums[i], curr_sum + nums[i])
max_sum = max(max_sum, curr_sum)
return max_sum
5. 最长递增子序列(LIS)
状态:dp[i] = 以nums[i]结尾的最长递增子序列长度
转移:dp[i] = max(dp[j] + 1) for j < i if nums[j] < nums[i]
时间:O(n²)
def lengthOfLIS(nums):
if not nums: return 0
dp = [1] * len(nums)
for i in range(len(nums)):
for j in range(i):
if nums[j] < nums[i]:
dp[i] = max(dp[i], dp[j] + 1)
return max(dp)
🎯 DP解题套路
一维DP
# 模板
dp = [0] * (n + 1)
dp[0] = initial_value
for i in range(1, n + 1):
dp[i] = f(dp[i-1], dp[i-2], ...)
return dp[n]
二维DP
# 模板
dp = [[0] * (m + 1) for _ in range(n + 1)]
# 初始化
for i in range(n + 1):
dp[i][0] = ...
for j in range(m + 1):
dp[0][j] = ...
# 状态转移
for i in range(1, n + 1):
for j in range(1, m + 1):
dp[i][j] = f(dp[i-1][j], dp[i][j-1], ...)
return dp[n][m]
📚 LeetCode练习
入门
进阶
💡 优化技巧
1. 空间优化
# 只依赖前一个状态
dp[i] = f(dp[i-1])
# 优化为一个变量
# 只依赖前两个状态
dp[i] = f(dp[i-1], dp[i-2])
# 优化为两个变量
# 只依赖上一行
dp[i][j] = f(dp[i-1][j], dp[i][j-1])
# 优化为一维数组
2. 降维技巧
# 二维改一维
for i in range(n):
for j in range(m):
dp[i][j] = f(dp[i-1][j], dp[i][j-1])
# 优化为
dp = [0] * m
for i in range(n):
for j in range(m):
dp[j] = f(dp[j], dp[j-1])
🎯 思维导图
动态规划
├── 线性DP
│ ├── 单串:爬楼梯、打家劫舍、最大子数组
│ └── 双串:最长公共子序列、编辑距离
├── 区间DP
│ ├── 最长回文子串
│ └── 戳气球
├── 背包DP
│ ├── 0-1背包
│ ├── 完全背包
│ └── 多重背包
└── 状态机DP
├── 股票问题
└── 打家劫舍II
💡 学习建议
从简单开始:斐波那契→爬楼梯→打家劫舍
画表理解:手动模拟DP过程
总结模板:相同类型问题用相同套路
优化意识:能否降维?能否滚动数组?
大量练习:至少50道DP题
学习顺序
理解递归解法
加memo优化
改为DP数组
优化空间
总结模板
💻 完整代码实现
Python 实现
1"""
2动态规划基础问题实现
3"""
4
5# ========== 1. 斐波那契数列 ==========
6
7def fib_recursive(n):
8 """递归:O(2^n)"""
9 if n <= 1:
10 return n
11 return fib_recursive(n - 1) + fib_recursive(n - 2)
12
13
14def fib_memo(n, memo=None):
15 """记忆化递归:O(n)"""
16 if memo is None:
17 memo = {}
18 if n in memo:
19 return memo[n]
20 if n <= 1:
21 return n
22 memo[n] = fib_memo(n - 1, memo) + fib_memo(n - 2, memo)
23 return memo[n]
24
25
26def fib_dp(n):
27 """DP数组:O(n)"""
28 if n <= 1:
29 return n
30 dp = [0] * (n + 1)
31 dp[1] = 1
32 for i in range(2, n + 1):
33 dp[i] = dp[i - 1] + dp[i - 2]
34 return dp[n]
35
36
37def fib_optimized(n):
38 """空间优化:O(1)"""
39 if n <= 1:
40 return n
41 prev, curr = 0, 1
42 for _ in range(2, n + 1):
43 prev, curr = curr, prev + curr
44 return curr
45
46
47# ========== 2. 爬楼梯 ==========
48
49def climb_stairs(n):
50 """一次爬1或2级,n级有多少种方法"""
51 if n <= 1:
52 return 1
53 prev, curr = 1, 1
54 for _ in range(2, n + 1):
55 prev, curr = curr, prev + curr
56 return curr
57
58
59# ========== 3. 打家劫舍 ==========
60
61def rob(nums):
62 """不能偷相邻房屋"""
63 if not nums:
64 return 0
65 if len(nums) == 1:
66 return nums[0]
67
68 prev, curr = 0, 0
69 for num in nums:
70 prev, curr = curr, max(curr, prev + num)
71 return curr
72
73
74# ========== 4. 最大子数组和 ==========
75
76def max_sub_array(nums):
77 """Kadane算法:O(n)"""
78 max_sum = nums[0]
79 curr_sum = nums[0]
80
81 for i in range(1, len(nums)):
82 curr_sum = max(nums[i], curr_sum + nums[i])
83 max_sum = max(max_sum, curr_sum)
84
85 return max_sum
86
87
88# ========== 5. 最长递增子序列 ==========
89
90def length_of_lis(nums):
91 """O(n²)解法"""
92 if not nums:
93 return 0
94
95 dp = [1] * len(nums)
96
97 for i in range(len(nums)):
98 for j in range(i):
99 if nums[j] < nums[i]:
100 dp[i] = max(dp[i], dp[j] + 1)
101
102 return max(dp)
103
104
105def length_of_lis_optimized(nums):
106 """二分优化:O(n log n)"""
107 if not nums:
108 return 0
109
110 tails = [] # tails[i]表示长度为i+1的LIS的最小末尾元素
111
112 for num in nums:
113 # 二分查找插入位置
114 left, right = 0, len(tails)
115 while left < right:
116 mid = (left + right) // 2
117 if tails[mid] < num:
118 left = mid + 1
119 else:
120 right = mid
121
122 if left == len(tails):
123 tails.append(num)
124 else:
125 tails[left] = num
126
127 return len(tails)
128
129
130# ========== 6. 零钱兑换 ==========
131
132def coin_change(coins, amount):
133 """最少硬币数"""
134 dp = [float('inf')] * (amount + 1)
135 dp[0] = 0
136
137 for i in range(1, amount + 1):
138 for coin in coins:
139 if i >= coin:
140 dp[i] = min(dp[i], dp[i - coin] + 1)
141
142 return dp[amount] if dp[amount] != float('inf') else -1
143
144
145# ========== 7. 不同路径 ==========
146
147def unique_paths(m, n):
148 """m×n网格,从左上到右下有多少路径"""
149 dp = [[0] * n for _ in range(m)]
150
151 # 初始化第一行和第一列
152 for i in range(m):
153 dp[i][0] = 1
154 for j in range(n):
155 dp[0][j] = 1
156
157 # 状态转移
158 for i in range(1, m):
159 for j in range(1, n):
160 dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
161
162 return dp[m - 1][n - 1]
163
164
165def unique_paths_optimized(m, n):
166 """空间优化:O(n)"""
167 dp = [1] * n
168
169 for i in range(1, m):
170 for j in range(1, n):
171 dp[j] += dp[j - 1]
172
173 return dp[n - 1]
174
175
176# ========== 8. 最小路径和 ==========
177
178def min_path_sum(grid):
179 """网格中从左上到右下的最小路径和"""
180 if not grid:
181 return 0
182
183 m, n = len(grid), len(grid[0])
184 dp = [[0] * n for _ in range(m)]
185
186 # 初始化
187 dp[0][0] = grid[0][0]
188 for i in range(1, m):
189 dp[i][0] = dp[i - 1][0] + grid[i][0]
190 for j in range(1, n):
191 dp[0][j] = dp[0][j - 1] + grid[0][j]
192
193 # 状态转移
194 for i in range(1, m):
195 for j in range(1, n):
196 dp[i][j] = min(dp[i - 1][j], dp[i][j - 1]) + grid[i][j]
197
198 return dp[m - 1][n - 1]
199
200
201def demo():
202 """演示DP算法"""
203 print("=== 动态规划基础演示 ===\n")
204
205 # 斐波那契
206 n = 10
207 print(f"斐波那契第{n}项:")
208 print(f" 递归: {fib_recursive(n)}")
209 print(f" 记忆化: {fib_memo(n)}")
210 print(f" DP: {fib_dp(n)}")
211 print(f" 优化: {fib_optimized(n)}\n")
212
213 # 爬楼梯
214 n = 5
215 print(f"爬{n}级楼梯的方法数: {climb_stairs(n)}\n")
216
217 # 打家劫舍
218 nums = [2, 7, 9, 3, 1]
219 print(f"打家劫舍 {nums}: {rob(nums)}\n")
220
221 # 最大子数组和
222 nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4]
223 print(f"最大子数组和 {nums}: {max_sub_array(nums)}\n")
224
225 # LIS
226 nums = [10, 9, 2, 5, 3, 7, 101, 18]
227 print(f"最长递增子序列 {nums}: {length_of_lis(nums)}\n")
228
229 # 零钱兑换
230 coins, amount = [1, 2, 5], 11
231 print(f"零钱兑换 coins={coins}, amount={amount}: {coin_change(coins, amount)}\n")
232
233 # 不同路径
234 m, n = 3, 7
235 print(f"网格 {m}×{n} 不同路径数: {unique_paths(m, n)}")
236
237
238if __name__ == '__main__':
239 demo()
240
C++ 实现
1/**
2 * 动态规划基础问题实现
3 */
4
5#include <iostream>
6#include <vector>
7#include <algorithm>
8#include <climits>
9
10using namespace std;
11
12// 斐波那契数列
13int fib(int n) {
14 if (n <= 1) return n;
15
16 int prev = 0, curr = 1;
17 for (int i = 2; i <= n; i++) {
18 int temp = curr;
19 curr = prev + curr;
20 prev = temp;
21 }
22 return curr;
23}
24
25// 爬楼梯
26int climbStairs(int n) {
27 if (n <= 1) return 1;
28
29 int prev = 1, curr = 1;
30 for (int i = 2; i <= n; i++) {
31 int temp = curr;
32 curr = prev + curr;
33 prev = temp;
34 }
35 return curr;
36}
37
38// 打家劫舍
39int rob(const vector<int>& nums) {
40 if (nums.empty()) return 0;
41 if (nums.size() == 1) return nums[0];
42
43 int prev = 0, curr = 0;
44 for (int num : nums) {
45 int temp = curr;
46 curr = max(curr, prev + num);
47 prev = temp;
48 }
49 return curr;
50}
51
52// 最大子数组和
53int maxSubArray(const vector<int>& nums) {
54 int maxSum = nums[0];
55 int currSum = nums[0];
56
57 for (int i = 1; i < nums.size(); i++) {
58 currSum = max(nums[i], currSum + nums[i]);
59 maxSum = max(maxSum, currSum);
60 }
61
62 return maxSum;
63}
64
65// 最长递增子序列
66int lengthOfLIS(const vector<int>& nums) {
67 if (nums.empty()) return 0;
68
69 vector<int> dp(nums.size(), 1);
70
71 for (int i = 0; i < nums.size(); i++) {
72 for (int j = 0; j < i; j++) {
73 if (nums[j] < nums[i]) {
74 dp[i] = max(dp[i], dp[j] + 1);
75 }
76 }
77 }
78
79 return *max_element(dp.begin(), dp.end());
80}
81
82// 零钱兑换
83int coinChange(const vector<int>& coins, int amount) {
84 vector<int> dp(amount + 1, INT_MAX);
85 dp[0] = 0;
86
87 for (int i = 1; i <= amount; i++) {
88 for (int coin : coins) {
89 if (i >= coin && dp[i - coin] != INT_MAX) {
90 dp[i] = min(dp[i], dp[i - coin] + 1);
91 }
92 }
93 }
94
95 return dp[amount] == INT_MAX ? -1 : dp[amount];
96}
97
98// 不同路径
99int uniquePaths(int m, int n) {
100 vector<int> dp(n, 1);
101
102 for (int i = 1; i < m; i++) {
103 for (int j = 1; j < n; j++) {
104 dp[j] += dp[j - 1];
105 }
106 }
107
108 return dp[n - 1];
109}
110
111int main() {
112 cout << "=== 动态规划基础演示 ===" << endl << endl;
113
114 // 斐波那契
115 int n = 10;
116 cout << "斐波那契第" << n << "项: " << fib(n) << endl << endl;
117
118 // 爬楼梯
119 n = 5;
120 cout << "爬" << n << "级楼梯的方法数: " << climbStairs(n) << endl << endl;
121
122 // 打家劫舍
123 vector<int> nums = {2, 7, 9, 3, 1};
124 cout << "打家劫舍 [2,7,9,3,1]: " << rob(nums) << endl << endl;
125
126 // 最大子数组和
127 vector<int> nums2 = {-2, 1, -3, 4, -1, 2, 1, -5, 4};
128 cout << "最大子数组和: " << maxSubArray(nums2) << endl << endl;
129
130 // LIS
131 vector<int> nums3 = {10, 9, 2, 5, 3, 7, 101, 18};
132 cout << "最长递增子序列长度: " << lengthOfLIS(nums3) << endl << endl;
133
134 // 零钱兑换
135 vector<int> coins = {1, 2, 5};
136 int amount = 11;
137 cout << "零钱兑换 coins=[1,2,5], amount=11: "
138 << coinChange(coins, amount) << endl << endl;
139
140 // 不同路径
141 cout << "网格 3×7 不同路径数: " << uniquePaths(3, 7) << endl;
142
143 return 0;
144}