动态规划# 01背包
问题描述
有 n n n 个物品,每个物品有重量 w i w_i wi 和价值 v i v_i vi。现在有一个容量为 W W W 的背包,每个物品要么选要么不选(0-1),求解在不超过背包容量的情况下,能够装入物品的最大价值。
动态规划解法
状态定义
- d p [ i ] [ j ] dp[i][j] dp[i][j] 表示考虑前 i i i 个物品,背包容量为 j j j 时能获得的最大价值
状态转移方程
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i]] + v[i]) if j >= w[i]
dp[i][j] = dp[i-1][j] if j < w[i]
初始状态
- d p [ 0 ] [ j ] = 0 dp[0][j] = 0 dp[0][j]=0 表示不选任何物品时的价值为0
- d p [ i ] [ 0 ] = 0 dp[i][0] = 0 dp[i][0]=0 表示背包容量为0时的价值为0
代码实现
C++ 实现(二维)
int knapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<vector<int>> dp(n + 1, vector<int>(W + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= W; j++) {
if (j >= w[i-1]) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j-w[i-1]] + v[i-1]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][W];
}
C++ 实现(一维优化)
int knapsack(vector<int>& w, vector<int>& v, int W) {
int n = w.size();
vector<int> dp(W + 1, 0);
for (int i = 0; i < n; i++) {
for (int j = W; j >= w[i]; j--) {
dp[j] = max(dp[j], dp[j-w[i]] + v[i]);
}
}
return dp[W];
}
Java 实现
class Solution {
public int knapsack(int[] w, int[] v, int W) {
int n = w.length;
int[] dp = new int[W + 1];
for (int i = 0; i < n; i++) {
for (int j = W; j >= w[i]; j--) {
dp[j] = Math.max(dp[j], dp[j-w[i]] + v[i]);
}
}
return dp[W];
}
}
Python 实现
def knapsack(w: List[int], v: List[int], W: int) -> int:
n = len(w)
dp = [0] * (W + 1)
for i in range(n):
for j in range(W, w[i]-1, -1):
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
return dp[W]
空间优化
-
二维转一维:
- 观察到当前状态只依赖于上一行的状态
- 可以用一维数组滚动更新
- 必须逆序遍历容量,防止状态被覆盖
-
初始化优化:
- 一维数组只需要初始化一次
- 所有位置初始化为0即可
时间复杂度分析
- 时间复杂度: O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(n⋅W)
- 空间复杂度:
- 二维实现: O ( n ⋅ W ) \mathcal{O}(n \cdot W) O(n⋅W)
- 一维实现: O ( W ) \mathcal{O}(W) O(W)
变体问题
- 完全背包:每个物品可以选无限次
- 多重背包:每个物品有特定的数量限制
- 分组背包:物品分组,每组最多选一个
- 二维费用背包:物品有两种限制条件
- 依赖背包:物品之间有依赖关系
应用场景
- 资源分配问题
- 投资组合优化
- 任务调度问题
- 物流配送规划
- 切割问题优化
注意事项
- 数组大小的分配
- 边界条件的处理
- 逆序遍历的必要性
- 状态转移的正确性
- 初始化的合理性
经典例题
动态规划(Dynamic Programming,简称 DP)是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的算法思想。
基本概念
核心思想
- 最优子结构:问题的最优解包含子问题的最优解
- 重叠子问题:子问题会重复出现,可以保存子问题的解
- 无后效性:某阶段的状态确定后,后续状态的演变不再受此前各状态及决策的影响
解题步骤
- 确定状态和选择
- 明确 dp 数组的定义
- 找出状态转移方程
- 确定初始条件和边界情况
- 确定遍历顺序
常见类型
线性 DP
-
单序列问题
- 最长上升子序列
- 最大子数组和
- 打家劫舍
-
双序列问题
- 最长公共子序列
- 编辑距离
- 正则表达式匹配
区间 DP
- 最长回文子序列
- 戳气球
- 矩阵链乘法
背包 DP
- 0-1背包
- 完全背包
- 多重背包
- 分组背包
状态压缩 DP
- 旅行商问题
- 集合划分问题
- 状态表示与压缩
树形 DP
- 树的最大独立集
- 树的最小支配集
- 树的直径
数位 DP
- 数字计数
- 数字染色
- 数字游戏
优化技巧
空间优化
- 滚动数组
- 状态压缩
- 一维化处理
时间优化
- 预处理
- 单调队列/单调栈
- 四边形不等式
解题技巧
状态设计
- 考虑问题的最后一步
- 化整为零,分解问题
- 寻找状态表示方式
方程推导
- 考虑所有可能的转移
- 确保转移的完备性
- 验证转移的正确性
边界处理
- 初始状态的设定
- 特殊情况的处理
- 数组越界的预防
常见误区
- 贪心与动态规划的混淆
- 递归与动态规划的区别
- 状态设计过于复杂
- 遗漏重要状态转移
练习建议
- 从简单问题开始
- 多画状态转移图
- 注重空间时间优化
- 总结相似问题规律
复杂度分析
时间复杂度
- 由状态数量和状态转移决定
- 通常为 O ( n 2 ) \mathcal{O}(n^2) O(n2) 到 O ( n 3 ) \mathcal{O}(n^3) O(n3)
- 状态压缩可能达到 O ( 2 n ) \mathcal{O}(2^n) O(2n)
空间复杂度
- 基本为 O ( n ) \mathcal{O}(n) O(n) 或 O ( n 2 ) \mathcal{O}(n^2) O(n2)
- 可通过滚动数组优化
- 某些问题可降至 O ( 1 ) \mathcal{O}(1) O(1)
727

被折叠的 条评论
为什么被折叠?



