背包问题
一、背包问题基础概念
核心问题:给定一组物品(重量和价值)和一个容量有限的背包,如何选择物品使得总重量不超过背包容量且总价值最大。
二、01背包问题
特点:每个物品只能选0次或1次
1. 问题分析
- 状态定义:
dp[i][j]表示前i个物品在容量j时的最大价值 - 状态转移:
- 不选第i个物品:
dp[i][j] = dp[i-1][j] - 选第i个物品:
dp[i][j] = dp[i-1][j-w[i]] + v[i]
- 不选第i个物品:
2. 二维数组实现
int[] weights = {2, 3, 4, 5};
int[] values = {3, 4, 5, 6};
int capacity = 8;
int[][] dp = new int[n+1][capacity+1];
for(int i=1; i<=n; i++){
for(int j=0; j<=capacity; j++){
if(j >= weights[i-1]){
dp[i][j] = Math.max(dp[i-1][j],
dp[i-1][j-weights[i-1]] + values[i-1]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
3. 一维空间优化(滚动数组)
int[] dp = new int[capacity+1];
for(int i=0; i<n; i++){
for(int j=capacity; j>=weights[i]; j--){ // 逆序遍历
dp[j] = Math.max(dp[j], dp[j-weights[i]] + values[i]);
}
}
关键点:逆序更新防止重复选择
三、完全背包问题
特点:物品可以选无限次
1. 状态转移方程
dp[j] = max(dp[j], dp[j-w[i]] + v[i])
2. 代码实现(与01背包仅循环顺序不同)
int[] dp = new int[capacity+1];
for(int i=0; i<n; i++){
for(int j=weights[i]; j<=capacity; j++){ // 正序遍历
dp[j] = Math.max(dp[j], dp[j-weights[i]] + values[i]);
}
}
四、多重背包问题
特点:每个物品最多选s次
1. 基础解法(转化为01背包)
// 展开为多个相同物品
List<Integer> newWeights = new ArrayList<>();
List<Integer> newValues = new ArrayList<>();
for(int i=0; i<n; i++){
int s = limits[i];
int k=1;
while(k <= s){
newWeights.add(weights[i]*k);
newValues.add(values[i]*k);
s -= k;
k *= 2;
}
if(s > 0){
newWeights.add(weights[i]*s);
newValues.add(values[i]*s);
}
}
// 然后使用01背包解法
2. 二进制优化原理
将数量s分解为1,2,4,...,2^k的系数组合,将O(n)复杂度降为O(logn)
五、单调队列优化多重背包
优化核心:利用滑动窗口最大值原理
1. 状态转移分析
对于每个余数r(0 ≤ r < w[i]),状态转移方程为:
dp[j] = max(dp[j - k*w[i]] + k*v[i]),其中k ∈ [0, s[i]]
2. Java实现
int[] dp = new int[capacity+1];
for(int i=0; i<n; i++){
int w = weights[i], v = values[i], s = limits[i];
for(int r=0; r<w; r++){ // 枚举余数
Deque<Integer> deque = new ArrayDeque<>();
for(int j=r; j<=capacity; j+=w){
// 维护队列单调性
while(!deque.isEmpty() &&
dp[j] - j/w*v >= dp[deque.peekLast()] - deque.peekLast()/w*v){
deque.pollLast();
}
deque.addLast(j);
// 移除过期元素
while(deque.peekFirst() < j - s*w){
deque.pollFirst();
}
dp[j] = Math.max(dp[j], dp[deque.peekFirst()] + (j - deque.peekFirst())/w * v);
}
}
}
六、二维费用背包
特点:物品有两种费用限制(如重量+体积)
1. 状态定义
dp[i][j][k] 表示前i个物品,费用1为j,费用2为k时的最大价值
2. 优化实现(三维→二维)
int[][] dp = new int[capacity1+1][capacity2+1];
for(int i=0; i<n; i++){
int w1 = weight[i], w2 = volume[i], v = values[i];
for(int j=capacity1; j>=w1; j--){
for(int k=capacity2; k>=w2; k--){
dp[j][k] = Math.max(dp[j][k], dp[j-w1][k-w2] + v);
}
}
}
七、分组背包问题
特点:物品分组,每组只能选一个
1. 状态转移
int[][] dp = new int[groups+1][capacity+1];
for(int g=1; g<=groups; g++){
for(int j=capacity; j>=0; j--){
for(Item item : group[g]){ // 遍历组内物品
if(j >= item.weight){
dp[g][j] = Math.max(dp[g][j],
dp[g-1][j-item.weight] + item.value);
}
}
}
}
2. 优化为一维数组
int[] dp = new int[capacity+1];
for(int g=0; g<groups; g++){
for(int j=capacity; j>=0; j--){
for(Item item : group[g]){
if(j >= item.weight){
dp[j] = Math.max(dp[j], dp[j-item.weight] + item.value);
}
}
}
}
八、总结对比
| 背包类型 | 核心特征 | 循环顺序关键点 |
|---|---|---|
| 01背包 | 每个物品选1次 | 容量逆序 |
| 完全背包 | 物品无限选 | 容量正序 |
| 多重背包 | 物品有限选 | 二进制拆分或单调队列 |
| 二维费用背包 | 双重限制条件 | 双逆序循环 |
| 分组背包 | 每组选一个 | 组外逆序,组内任意顺序 |

浙公网安备 33010602011771号