背包问题

一、背包问题基础概念

核心问题:给定一组物品(重量和价值)和一个容量有限的背包,如何选择物品使得总重量不超过背包容量且总价值最大。


二、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]

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次 容量逆序
完全背包 物品无限选 容量正序
多重背包 物品有限选 二进制拆分或单调队列
二维费用背包 双重限制条件 双逆序循环
分组背包 每组选一个 组外逆序,组内任意顺序
posted @ 2025-02-24 22:10  咋还没来  阅读(41)  评论(0)    收藏  举报