08-背包问题

💡 核心结论

背包问题本质

  • 定义:在容量限制下,选择物品使价值最大

  • 核心:动态规划的经典应用

  • 关键:每个物品选或不选,累积最优解

  • 难点:状态定义、空间优化

  • 应用:资源分配、项目选择、切割问题

三种背包对比

类型

特点

状态转移

遍历顺序

0-1背包

每个物品最多1个

max(不选, 选)

逆序

完全背包

每个物品无限个

max(不选, 选)

正序

多重背包

每个物品k个

二进制优化

-

0-1背包核心

状态:dp[i][j] = 前i个物品,容量j的最大价值
转移:dp[i][j] = max(
    dp[i-1][j],              # 不选第i个
    dp[i-1][j-w[i]] + v[i]   # 选第i个
)
空间优化:一维数组,逆序遍历

完全背包核心

状态:dp[i][j] = 前i种物品,容量j的最大价值  
转移:dp[i][j] = max(
    dp[i-1][j],          # 不选第i种
    dp[i][j-w[i]] + v[i] # 选第i种(注意是dp[i]不是dp[i-1])
)
空间优化:一维数组,正序遍历

🎯 0-1背包

二维DP

def knapsack_01(weights, values, capacity):
    n = len(weights)
    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
    
    for i in range(1, n + 1):
        for j in range(capacity + 1):
            if j < weights[i-1]:
                dp[i][j] = dp[i-1][j]
            else:
                dp[i][j] = max(
                    dp[i-1][j],
                    dp[i-1][j - weights[i-1]] + values[i-1]
                )
    
    return dp[n][capacity]

一维DP(空间优化)

def knapsack_01_optimized(weights, values, capacity):
    dp = [0] * (capacity + 1)
    
    for i in range(len(weights)):
        # 逆序遍历(避免物品被重复选择)
        for j in range(capacity, weights[i] - 1, -1):
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
    
    return dp[capacity]

🔄 完全背包

一维DP

def knapsack_complete(weights, values, capacity):
    dp = [0] * (capacity + 1)
    
    for i in range(len(weights)):
        # 正序遍历(允许物品被重复选择)
        for j in range(weights[i], capacity + 1):
            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
    
    return dp[capacity]

🎁 多重背包

二进制优化

def knapsack_multiple(weights, values, counts, capacity):
    # 转换为0-1背包
    new_weights = []
    new_values = []
    
    for i in range(len(weights)):
        k = 1
        while k <= counts[i]:
            new_weights.append(weights[i] * k)
            new_values.append(values[i] * k)
            counts[i] -= k
            k *= 2
        if counts[i] > 0:
            new_weights.append(weights[i] * counts[i])
            new_values.append(values[i] * counts[i])
    
    return knapsack_01_optimized(new_weights, new_values, capacity)

🎯 背包变种

1. 恰好装满

# 初始化
dp = [-inf] * (capacity + 1)
dp[0] = 0  # 容量0价值0

2. 求方案数

# 状态:dp[i] = 容量i的方案数
dp[j] += dp[j - weight[i]]  # 累加方案数

3. 求具体方案

# 记录选择
choice = [[False] * (capacity + 1) for _ in range(n + 1)]

# 回溯方案
result = []
j = capacity
for i in range(n, 0, -1):
    if choice[i][j]:
        result.append(i - 1)
        j -= weights[i - 1]

📚 LeetCode练习

0-1背包

完全背包

💡 解题模板

0-1背包模板

dp = [0] * (capacity + 1)
for i in range(n):
    for j in range(capacity, weights[i] - 1, -1):
        dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]

完全背包模板

dp = [0] * (capacity + 1)
for i in range(n):
    for j in range(weights[i], capacity + 1):
        dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
return dp[capacity]

🎯 关键区别

0-1背包 vs 完全背包唯一区别

  • 0-1:for j in range(capacity, w-1, -1) 逆序

  • 完全:for j in range(w, capacity+1) 正序

为什么?

  • 逆序:保证每个物品只用一次

  • 正序:允许物品重复使用

💻 完整代码实现

Python 实现

  1"""
  2背包问题实现
  3"""
  4
  5# ========== 0-1背包 ==========
  6
  7def knapsack_01(weights, values, capacity):
  8    """0-1背包(二维DP)"""
  9    n = len(weights)
 10    dp = [[0] * (capacity + 1) for _ in range(n + 1)]
 11    
 12    for i in range(1, n + 1):
 13        for j in range(capacity + 1):
 14            if j < weights[i-1]:
 15                dp[i][j] = dp[i-1][j]
 16            else:
 17                dp[i][j] = max(
 18                    dp[i-1][j],
 19                    dp[i-1][j - weights[i-1]] + values[i-1]
 20                )
 21    
 22    return dp[n][capacity]
 23
 24
 25def knapsack_01_optimized(weights, values, capacity):
 26    """0-1背包(一维DP优化)"""
 27    dp = [0] * (capacity + 1)
 28    
 29    for i in range(len(weights)):
 30        # 逆序遍历
 31        for j in range(capacity, weights[i] - 1, -1):
 32            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
 33    
 34    return dp[capacity]
 35
 36
 37# ========== 完全背包 ==========
 38
 39def knapsack_complete(weights, values, capacity):
 40    """完全背包(物品可重复选择)"""
 41    dp = [0] * (capacity + 1)
 42    
 43    for i in range(len(weights)):
 44        # 正序遍历
 45        for j in range(weights[i], capacity + 1):
 46            dp[j] = max(dp[j], dp[j - weights[i]] + values[i])
 47    
 48    return dp[capacity]
 49
 50
 51# ========== 多重背包 ==========
 52
 53def knapsack_multiple(weights, values, counts, capacity):
 54    """多重背包(每个物品有数量限制)"""
 55    # 二进制优化:转换为0-1背包
 56    new_weights = []
 57    new_values = []
 58    
 59    for i in range(len(weights)):
 60        count = counts[i]
 61        k = 1
 62        while k <= count:
 63            new_weights.append(weights[i] * k)
 64            new_values.append(values[i] * k)
 65            count -= k
 66            k *= 2
 67        if count > 0:
 68            new_weights.append(weights[i] * count)
 69            new_values.append(values[i] * count)
 70    
 71    return knapsack_01_optimized(new_weights, new_values, capacity)
 72
 73
 74# ========== 应用:分割等和子集 ==========
 75
 76def can_partition(nums):
 77    """判断能否分割成两个和相等的子集"""
 78    total = sum(nums)
 79    if total % 2:
 80        return False
 81    
 82    target = total // 2
 83    dp = [False] * (target + 1)
 84    dp[0] = True
 85    
 86    for num in nums:
 87        for j in range(target, num - 1, -1):
 88            dp[j] = dp[j] or dp[j - num]
 89    
 90    return dp[target]
 91
 92
 93# ========== 应用:零钱兑换 ==========
 94
 95def coin_change(coins, amount):
 96    """最少硬币数(完全背包)"""
 97    dp = [float('inf')] * (amount + 1)
 98    dp[0] = 0
 99    
100    for coin in coins:
101        for j in range(coin, amount + 1):
102            dp[j] = min(dp[j], dp[j - coin] + 1)
103    
104    return dp[amount] if dp[amount] != float('inf') else -1
105
106
107def coin_change_ways(coins, amount):
108    """组成金额的方案数(完全背包)"""
109    dp = [0] * (amount + 1)
110    dp[0] = 1
111    
112    for coin in coins:
113        for j in range(coin, amount + 1):
114            dp[j] += dp[j - coin]
115    
116    return dp[amount]
117
118
119def demo():
120    """演示背包问题"""
121    print("=== 背包问题演示 ===\n")
122    
123    # 0-1背包
124    weights = [2, 3, 4, 5]
125    values = [3, 4, 5, 6]
126    capacity = 8
127    
128    print(f"0-1背包:")
129    print(f"  重量: {weights}")
130    print(f"  价值: {values}")
131    print(f"  容量: {capacity}")
132    print(f"  最大价值: {knapsack_01(weights, values, capacity)}")
133    print(f"  最大价值(优化): {knapsack_01_optimized(weights, values, capacity)}\n")
134    
135    # 完全背包
136    print(f"完全背包:")
137    print(f"  最大价值: {knapsack_complete(weights, values, capacity)}\n")
138    
139    # 多重背包
140    counts = [1, 2, 3, 1]
141    print(f"多重背包:")
142    print(f"  数量: {counts}")
143    print(f"  最大价值: {knapsack_multiple(weights, values, counts, capacity)}\n")
144    
145    # 分割等和子集
146    nums = [1, 5, 11, 5]
147    print(f"分割等和子集 {nums}:")
148    print(f"  能否分割: {can_partition(nums)}\n")
149    
150    # 零钱兑换
151    coins, amount = [1, 2, 5], 11
152    print(f"零钱兑换:")
153    print(f"  硬币: {coins}, 金额: {amount}")
154    print(f"  最少硬币数: {coin_change(coins, amount)}")
155    print(f"  方案数: {coin_change_ways(coins, amount)}")
156
157
158if __name__ == '__main__':
159    demo()
160

C++ 实现

  1/**
  2 * 背包问题实现
  3 */
  4
  5#include <iostream>
  6#include <vector>
  7#include <algorithm>
  8
  9using namespace std;
 10
 11// 0-1背包(二维DP)
 12int knapsack01(const vector<int>& weights, const vector<int>& values, int capacity) {
 13    int n = weights.size();
 14    vector<vector<int>> dp(n + 1, vector<int>(capacity + 1, 0));
 15    
 16    for (int i = 1; i <= n; i++) {
 17        for (int j = 0; j <= capacity; j++) {
 18            if (j < weights[i-1]) {
 19                dp[i][j] = dp[i-1][j];
 20            } else {
 21                dp[i][j] = max(
 22                    dp[i-1][j],
 23                    dp[i-1][j - weights[i-1]] + values[i-1]
 24                );
 25            }
 26        }
 27    }
 28    
 29    return dp[n][capacity];
 30}
 31
 32// 0-1背包(一维优化)
 33int knapsack01Optimized(const vector<int>& weights, const vector<int>& values, int capacity) {
 34    vector<int> dp(capacity + 1, 0);
 35    
 36    for (int i = 0; i < weights.size(); i++) {
 37        // 逆序遍历
 38        for (int j = capacity; j >= weights[i]; j--) {
 39            dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
 40        }
 41    }
 42    
 43    return dp[capacity];
 44}
 45
 46// 完全背包
 47int knapsackComplete(const vector<int>& weights, const vector<int>& values, int capacity) {
 48    vector<int> dp(capacity + 1, 0);
 49    
 50    for (int i = 0; i < weights.size(); i++) {
 51        // 正序遍历
 52        for (int j = weights[i]; j <= capacity; j++) {
 53            dp[j] = max(dp[j], dp[j - weights[i]] + values[i]);
 54        }
 55    }
 56    
 57    return dp[capacity];
 58}
 59
 60// 零钱兑换(完全背包)
 61int coinChange(const vector<int>& coins, int amount) {
 62    vector<int> dp(amount + 1, INT_MAX);
 63    dp[0] = 0;
 64    
 65    for (int coin : coins) {
 66        for (int j = coin; j <= amount; j++) {
 67            if (dp[j - coin] != INT_MAX) {
 68                dp[j] = min(dp[j], dp[j - coin] + 1);
 69            }
 70        }
 71    }
 72    
 73    return dp[amount] == INT_MAX ? -1 : dp[amount];
 74}
 75
 76// 零钱兑换II(方案数)
 77int coinChangeWays(const vector<int>& coins, int amount) {
 78    vector<int> dp(amount + 1, 0);
 79    dp[0] = 1;
 80    
 81    for (int coin : coins) {
 82        for (int j = coin; j <= amount; j++) {
 83            dp[j] += dp[j - coin];
 84        }
 85    }
 86    
 87    return dp[amount];
 88}
 89
 90// 分割等和子集
 91bool canPartition(const vector<int>& nums) {
 92    int sum = 0;
 93    for (int num : nums) sum += num;
 94    
 95    if (sum % 2) return false;
 96    
 97    int target = sum / 2;
 98    vector<bool> dp(target + 1, false);
 99    dp[0] = true;
100    
101    for (int num : nums) {
102        for (int j = target; j >= num; j--) {
103            dp[j] = dp[j] || dp[j - num];
104        }
105    }
106    
107    return dp[target];
108}
109
110int main() {
111    cout << "=== 背包问题演示 ===" << endl << endl;
112    
113    // 0-1背包
114    vector<int> weights = {2, 3, 4, 5};
115    vector<int> values = {3, 4, 5, 6};
116    int capacity = 8;
117    
118    cout << "0-1背包:" << endl;
119    cout << "  重量: [2,3,4,5]" << endl;
120    cout << "  价值: [3,4,5,6]" << endl;
121    cout << "  容量: 8" << endl;
122    cout << "  最大价值: " << knapsack01(weights, values, capacity) << endl;
123    cout << "  最大价值(优化): " << knapsack01Optimized(weights, values, capacity) << endl << endl;
124    
125    // 完全背包
126    cout << "完全背包:" << endl;
127    cout << "  最大价值: " << knapsackComplete(weights, values, capacity) << endl << endl;
128    
129    // 零钱兑换
130    vector<int> coins = {1, 2, 5};
131    int amount = 11;
132    cout << "零钱兑换:" << endl;
133    cout << "  硬币: [1,2,5], 金额: 11" << endl;
134    cout << "  最少硬币数: " << coinChange(coins, amount) << endl;
135    cout << "  方案数: " << coinChangeWays(coins, amount) << endl << endl;
136    
137    // 分割等和子集
138    vector<int> nums = {1, 5, 11, 5};
139    cout << "分割等和子集 [1,5,11,5]:" << endl;
140    cout << "  能否分割: " << (canPartition(nums) ? "是" : "否") << endl;
141    
142    return 0;
143}