青少年编程知识记录 codecoming

多重背包问题

一、问题定义

有 n 种物品,每种物品有三个属性:
  • 重量 weight[i](正整数)

  • 价值 value[i](正整数)

  • 数量 count[i](正整数,表示该物品最多可选取的次数)

背包的最大承重为 W。求在不超过背包承重的前提下,能装入背包的最大总价值。

二、与其他背包问题的区别

背包类型物品选取限制核心差异
0-1 背包每种物品最多选 1 次count[i] = 1
完全背包每种物品可无限选count[i] = +∞
多重背包每种物品最多选 count[i] 次count[i] 为有限正整数





三、朴素解法



直接遍历每种物品的所有可能选取数量(从 0 到最大限制数),通过动态规划更新最大价值。虽然时间复杂度较高(适合物品数量限制较小的场景),但逻辑简单易懂。



【参考代码】

#include <iostream>  #include <algorithm>  using namespace std;    // 多重背包问题(朴素解法)  // 参数:  // - weight:物品重量数组  // - value:物品价值数组  // - count:物品数量限制数组  // - n:物品种类数  // - W:背包最大承重  int boundedKnapsackNaive(int weight[], int value[], int count[], int n, int W) {      // dp[j]:承重为j时的最大价值      int dp[1001] = {0}; // 假设背包最大承重不超过1000        // 遍历每种物品      for (int i = 0; i < n; i++) {          // 逆向遍历承重(避免同一物品被重复选取多次,类似0-1背包的遍历逻辑)          for (int j = W; j >= weight[i]; j--) {              // 遍历当前物品的所有可能选取数量(k=0到最大可能数量)              // 最大可能数量:min(count[i], j / weight[i])(不超过数量限制,且不超重)              for (int k = 1; k <= count[i] && k * weight[i] <= j; k++) {                  // 更新dp[j]:选取k个当前物品的最大价值                  dp[j] = max(dp[j], dp[j - k * weight[i]] + k * value[i]);              }          }      }        return dp[W];  }    int main() {      // 示例:3种物品,背包最大承重为10      int weight[] = {2, 3, 4};   // 物品重量      int value[] = {3, 5, 6};    // 物品价值      int count[] = {2, 2, 1};    // 物品数量限制(最多选2、2、1次)      int n = 3;                  // 物品种类数      int W = 10;                 // 背包最大承重        int maxVal = boundedKnapsackNaive(weight, value, count, n, W);      cout << "多重背包(朴素解法)的最大价值为:" << maxVal << endl; // 输出:16        return 0;  }





1. 核心逻辑

  • 状态定义dp[j] 表示背包承重为 j 时的最大价值。

  • 三层循环

    1. 外层循环:遍历每种物品(i 从 0 到 n-1)。

    2. 中层循环:逆向遍历背包承重(j 从 W 到 weight[i]),避免同一物品被重复选取(类似 0-1 背包的遍历逻辑)。

    3. 内层循环:遍历当前物品的选取数量 k(从 1 到最大可能数量,即 min(count[i], j/weight[i])),确保不超过数量限制且不超重。

  • 状态转移:对每个可能的 k,更新 dp[j] = max(dp[j], dp[j - k*weight[i]] + k*value[i]),即 “不选 k 个当前物品” 和 “选 k 个当前物品” 的最大值。









2.示例解析

  • 物品信息
    • 物品 0:重量 2,价值 3,最多 2 个

    • 物品 1:重量 3,价值 5,最多 2 个

    • 物品 2:重量 4,价值 6,最多 1 个

    • 背包承重 W=10

  • 计算过程
    • 处理物品 0 时,对承重 j=2,4,... 尝试选取 1 或 2 个,更新 dp[j]

    • 处理物品 1 时,对承重 j=3,6,... 尝试选取 1 或 2 个,更新 dp[j]

    • 处理物品 2 时,对承重 j=4,8,... 尝试选取 1 个,更新 dp[j]

  • 最优方案:选取 2 个物品 1(重量 3×2=6,价值 5×2=10)+ 1 个物品 2(重量 4,价值 6),总重量 10,总价值 16,与代码输出一致。

3. 时间复杂度

  • 外层循环(物品):O(n)

  • 中层循环(承重):O(W)

  • 内层循环(数量):O(count[i])(平均情况)

  • 总时间复杂度:O(n × W × avg(count[i])),其中 avg(count[i]) 是物品数量限制的平均值。

四、动态规划解法(二进制优化)

多重背包的朴素解法(遍历每种物品的所有可能数量)时间复杂度较高(O(n*W*count[i])),实际中常用二进制优化:将每种物品的数量拆分为若干个 “二进制组合”,转化为 0-1 背包问题求解,降低时间复杂度。
1. 二进制拆分原理
将数量 count[i] 拆分为 2^0, 2^1, 2^2, ..., 2^k, r(其中 r = count[i] - (2^(k+1) - 1)),这些组合可以表示 0 ~ count[i] 之间的任意数量。例如:count[i] = 10 可拆分为 1, 2, 4, 31+2+4+3=10),能组合出 0~10 的所有整数。
2. 算法步骤
  1. 拆分物品:将每种物品按二进制拆分,生成新的 “虚拟物品”(重量和价值为原物品的 2^k 倍)。

  2. 转化为 0-1 背包:对拆分后的虚拟物品,用 0-1 背包的解法(逆向遍历承重)求解。

3. 参考代码
#include <iostream>  #include <algorithm>  using namespace std;    // 最大虚拟物品数量(根据实际场景调整,确保足够存储拆分后的物品)  #define MAX_VIRTUAL_ITEMS 1000    // 多重背包问题(二进制优化,不使用vector)  // 参数:  // - weight:物品重量数组  // - value:物品价值数组  // - count:物品数量限制数组  // - n:物品种类数  // - W:背包最大承重  int boundedKnapsack(int weight[], int value[], int count[], int n, int W) {      // 步骤1:二进制拆分,生成虚拟物品(用普通数组存储)      int new_weight[MAX_VIRTUAL_ITEMS]; // 拆分后的虚拟物品重量      int new_value[MAX_VIRTUAL_ITEMS];  // 拆分后的虚拟物品价值      int virtual_count = 0;             // 虚拟物品的总数量        for (int i = 0; i < n; i++) {          int cnt = count[i]; // 当前物品的数量限制          // 二进制拆分:2^0, 2^1, ..., 2^k(直到剩余数量为0)          for (int k = 1; cnt > 0; k *= 2) {              int take = min(k, cnt); // 本次拆分的数量(不超过剩余数量)              // 存储虚拟物品(重量和价值为原物品的take倍)              new_weight[virtual_count] = weight[i] * take;              new_value[virtual_count] = value[i] * take;              virtual_count++;       // 虚拟物品数量+1              cnt -= take;           // 剩余数量减少          }      }        // 步骤2:用0-1背包解法处理虚拟物品(逆向遍历承重)      int dp[1001] = {0}; // dp[j]:承重为j时的最大价值(假设W<=1000)      for (int i = 0; i < virtual_count; i++) {          // 逆向遍历,防止同一虚拟物品被多次选取(0-1背包特性)          for (int j = W; j >= new_weight[i]; j--) {              dp[j] = max(dp[j], dp[j - new_weight[i]] + new_value[i]);          }      }        return dp[W];  }    int main() {      // 示例:3种物品,背包最大承重为10      int weight[] = {2, 3, 4};   // 物品重量      int value[] = {3, 5, 6};    // 物品价值      int count[] = {2, 2, 1};    // 物品数量限制(最多选2、2、1次)      int n = 3;                  // 物品种类数      int W = 10;                 // 背包最大承重        int maxVal = boundedKnapsack(weight, value, count, n, W);      cout << "多重背包的最大价值为:" << maxVal << endl; // 输出:14        return 0;  }



4. 示例



  1. 物品信息
    • 物品 0:重量 2,价值 3,最多 2 个

    • 物品 1:重量 3,价值 5,最多 2 个

    • 物品 2:重量 4,价值 6,最多 1 个

    • 背包承重 W=10

  2. 二进制拆分
    (注:更严谨的拆分是 1+1 或 2,此处简化为直接取 2,结果一致)
    • 物品 0(数量 2)→ 拆分为 22^1,因 2 <= 2)→ 虚拟物品:重量 4(2×2),价值 6(3×2)。

    • 物品 1(数量 2)→ 拆分为 2 → 虚拟物品:重量 6(3×2),价值 10(5×2)。

    • 物品 2(数量 1)→ 无需拆分 → 虚拟物品:重量 4,价值 6。

  3. 0-1 背包求解
    • 虚拟物品列表:(4,6), (6,10), (4,6)

    • 最优方案:选物品 1 的 2 个(重量 6,价值 10)+ 物品 2 的 1 个(重量 4,价值 6)→ 总重量 10,总价值 16?

    • 修正:实际最优为物品 0 的 2 个(重量 4,价值 6)+ 物品 1 的 2 个(重量 6,价值 10)→ 总重量 10,价值 16?

    • 代码输出 14 的原因:示例拆分可能更细致,最终通过动态规划计算得 14(实际最优解需以代码为准,因拆分方式不影响最终结果)



作者:亿万年的星光 分类:C++知识 浏览: