什么是动态规划
动态规划(Dynamic Programming,DP)是一种通过将复杂问题分解为重叠子问题,并存储子问题解以避免重复计算的优化算法。它适用于具有以下两个关键性质的问题:
最优子结构:问题的最优解包含子问题的最优解
重叠子问题:不同决策序列会重复求解相同的子问题
下面用一些例子(由浅入深)了解动态规划
int fib(int n) {
if(n <= 1) return n; // 基准条件:F(0)=0, F(1)=1
return fib(n-1) + fib(n-2); // 递归分解为两个子问题
}
代码解析:
int memo[100] = {0}; // 全局记忆数组,默认初始化为0
int fib_memo(int n) {
if(n <= 1) return n;
if(memo[n] != 0) // 检查是否已计算过
return memo[n];
return memo[n] = fib_memo(n-1) + fib_memo(n-2); // 计算结果并存储
}
代码解析:
int fib_tab(int n) {
if(n == 0) return 0;
int dp[n+1]; // 创建DP表
dp[0] = 0; // 初始化基础条件
dp[1] = 1;
for(int i=2; i<=n; ++i)
dp[i] = dp[i-1] + dp[i-2]; // 递推填充表格
return dp[n];
}
代码解析:
问题描述:给定两个字符串text1和text2,返回它们的最长公共子序列的长度
int lcs(string text1, string text2) {
int m = text1.size(), n = text2.size();
// 创建(m+1)x(n+1)的二维DP表,+1是为了处理空字符串的情况
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
for(int i=1; i<=m; ++i) {
for(int j=1; j<=n; ++j) {
if(text1[i-1] == text2[j-1]) // 字符匹配(注意索引偏移)
dp[i][j] = dp[i-1][j-1] + 1;
else // 不匹配时取两个可能方向的最大值
dp[i][j] = max(dp[i-1][j], dp[i][j-1]);
}
}
return dp[m][n];
}
代码解析:
dp[i][j]
表示text1前i个字符与text2前j个字符的LCS长度问题描述:给定物品重量数组wt和价值数组val,背包容量W,求能装的最大价值
int knapsack(int W, vector<int>& wt, vector<int>& val) {
int n = wt.size();
vector<int> dp(W+1, 0); // 一维DP数组优化空间
for(int i=0; i<n; ++i) { // 遍历每个物品
for(int w=W; w>=wt[i]; --w) { // 逆序更新防止覆盖
dp[w] = max(dp[w], // 不选当前物品
dp[w - wt[i]] + val[i]); // 选择当前物品
}
}
return dp[W];
}
代码解析:
dp[w]
表示容量为w时的最大价值问题描述:计算将word1转换成word2所需的最小操作次数(插入、删除、替换)
int minDistance(string word1, string word2) {
int m = word1.size(), n = word2.size();
vector<vector<int>> dp(m+1, vector<int>(n+1, 0));
// 初始化边界条件
for(int i=0; i<=m; ++i) dp[i][0] = i; // 删除i次
for(int j=0; j<=n; ++j) dp[0][j] = j; // 插入j次
for(int i=1; i<=m; ++i) {
for(int j=1; j<=n; ++j) {
if(word1[i-1] == word2[j-1]) { // 字符相同无需操作
dp[i][j] = dp[i-1][j-1];
} else { // 选择三种操作中的最小代价
dp[i][j] = 1 + min({dp[i-1][j], // 删除word1字符
dp[i][j-1], // 插入word2字符
dp[i-1][j-1]});// 替换字符
}
}
}
return dp[m][n];
}
代码解析:
dp[i][j]
表示转换前i个字符到前j个字符的最小操作数int fib_opt(int n) {
if(n == 0) return 0;
int prev = 0, curr = 1; // 初始值F(0)=0, F(1)=1
for(int i=2; i<=n; ++i) {
int next = prev + curr; // 计算下一个值
prev = curr; // 更新前一个值
curr = next; // 更新当前值
}
return curr;
}
优化原理:
// 二维原始版本
int dp[n+1][W+1];
// 优化为一维数组
vector<int> dp(W+1, 0);
优化原理:
确定问题变量维度:
常见状态定义模式:
分析子问题关系:
方程建立步骤:
(1) 列出所有可能的决策选项
(2) 计算每个决策对应的子问题解
(3) 选择最优决策并组合结果
边界条件处理:
特殊值初始化示例:
// 矩阵路径问题初始化第一行和第一列
for(int i=0; i<m; ++i) dp[i][0] = 1;
for(int j=0; j<n; ++j) dp[0][j] = 1;
问题描述:求整数数组中和最大的连续子数组
int maxSubArray(vector<int>& nums) {
int currMax = nums[0], globalMax = nums[0];
for(int i=1; i<nums.size(); ++i) {
// 决策:继续扩展子数组 or 重新开始
currMax = max(nums[i], currMax + nums[i]);
// 更新全局最大值
globalMax = max(globalMax, currMax);
}
return globalMax;
}
算法解析:
问题描述:m x n网格从左上角到右下角的唯一路径数
int uniquePaths(int m, int n) {
vector<vector<int>> dp(m, vector<int>(n, 1));
for(int i=1; i<m; ++i) {
for(int j=1; j<n; ++j) {
dp[i][j] = dp[i-1][j] + dp[i][j-1];
}
}
return dp[m-1][n-1];
}
算法解析:
// 在LCS代码中插入调试输出
for(auto& row : dp) {
for(int val : row) cout << val << " ";
cout << endl;
}
基本公式:状态数 × 每个状态的转移成本
多项式时间与伪多项式时间:
滚动数组技巧:
状态压缩技巧:
基础阶段(1-2周):
提高阶段(2-4周):
精通阶段(1-2月):
题目类型 | LeetCode题号 | 难度 |
---|---|---|
爬楼梯 | 70 | 简单 |
最长递增子序列 | 300 | 中等 |
零钱兑换 | 322 | 中等 |
正则表达式匹配 | 10 | 困难 |
买卖股票最佳时机 | 121/123 | 中等 |
int dp[n];
dp[0] = initial_value;
for(int i=1; i<n; ++i) {
dp[i] = compute(dp[...]);
}
return dp[n-1];
vector<vector<int>> dp(m, vector<int>(n, 0));
// 初始化边界
for(int i=0; i<m; ++i) dp[i][0] = ...;
for(int j=0; j<n; ++j) dp[0][j] = ...;
// 填充表格
for(int i=1; i<m; ++i) {
for(int j=1; j<n; ++j) {
dp[i][j] = compute(dp[i-1][j], dp[i][j-1], ...);
}
}
Q:如何判断一个问题是否可以用DP解决?
A:检查问题是否具有:
Q:DP和分治法的区别是什么?
A:分治法将问题分解为独立的子问题,而DP处理的是重叠的子问题
Q:如何处理环形结构问题?
A:常用技巧:
Q:如何选择记忆化递归还是迭代法?
A: