多重背包问题
2025-10-14
6
我们先明确一下多重背包问题的定义,然后我会给出思路、经典解法和优化方法,并附上代码示例。1. 问题定义多重背包问题(Multiple Knapsack Problem)是背包问题的一种变体:有一个容量为C的背包。有n种物品,第i种物品的重量为wi,价值为vi,数量为si(每件物品最多可选si件)。
我们先明确一下多重背包问题的定义,然后我会给出思路、经典解法和优化方法,并附上代码示例。

1. 问题定义

多重背包问题(Multiple Knapsack Problem)是背包问题的一种变体:

  • 有一个容量为 的背包。

  • 有 种物品,第 种物品的重量为 ,价值为 ,数量为 (每件物品最多可选 件)。

  • 目标:选择物品装入背包,使得总重量不超过 ,且总价值最大。


2. 与 0-1 背包、完全背包的区别

  • 0-1 背包:每种物品只有 1 件(选或不选)。

  • 完全背包:每种物品无限件。

  • 多重背包:每种物品有给定的有限件数 


3. 基本思路

3.1 直接转化为 0-1 背包

最直观的方法是把第 种物品的 件拆成 个独立物品,然后用 0-1 背包求解。
但这样物品总件数是 ,如果 很大,复杂度会很高。
时间复杂度:,不够高效。


3.2 二进制优化

核心思想:用二进制表示法将多个相同物品组合成“物品组”,使得用这些组可以组合出 0 到 之间的任意件数,且组数仅为 
做法
对第 种物品,将 拆成 这样的组(其中 ),每个新组的重量 = 组内物品个数 × ,价值 = 组内物品个数 × 
这样我们得到了 个新物品,每个新物品只能选一次(0-1 背包)。
之后用 0-1 背包方法求解。
时间复杂度


3.3 单调队列优化

更优的解法是使用单调队列,将复杂度降至 ,与 0-1 背包、完全背包的复杂度形式相同(但常数稍大)。
思路:利用滑动窗口最大值的思想,对每个余数类分别进行 DP 转移。
这里不展开细节,但它是多重背包的最优解法。

1. 二进制优化解法

思路

将每种物品的数量 拆分为二进制组合(如 1, 2, 4, ..., 剩余部分),转化为 0-1 背包问题。

代码

#include <iostream>

#include <vector>

#include <algorithm>

using namespace std;


int multipleKnapsackBinary(int C, vector<int>& w, vector<int>& v, vector<int>& s) {

    vector<int> new_w, new_v;


    // 二进制拆分

    for (int i = 0; i < w.size(); ++i) {

        int cnt = s[i];

        for (int k = 1; k <= cnt; k *= 2) {

            new_w.push_back(k * w[i]);

            new_v.push_back(k * v[i]);

            cnt -= k;

        }

        if (cnt > 0) {

            new_w.push_back(cnt * w[i]);

            new_v.push_back(cnt * v[i]);

        }

    }


    // 0-1 背包

    vector<int> dp(C + 1, 0);

    for (int i = 0; i < new_w.size(); ++i) {

        for (int j = C; j >= new_w[i]; --j) {

            dp[j] = max(dp[j], dp[j - new_w[i]] + new_v[i]);

        }

    }

    return dp[C];

}


int main() {

    int C = 10;

    vector<int> w = {2, 3, 4};

    vector<int> v = {3, 4, 5};

    vector<int> s = {3, 2, 1};  // 物品数量


    int result = multipleKnapsackBinary(C, w, v, s);

    cout << "最大价值(二进制优化): " << result << endl;  // 输出 13

    return 0;

}

2. 单调队列优化解法

思路

对每个余数类 ),用单调队列维护滑动窗口的最大值,优化动态规划转移。

代码

#include <iostream>

#include <vector>

#include <deque>

using namespace std;


int multipleKnapsackMonotonicQueue(int C, vector<int>& w, vector<int>& v, vector<int>& s) {

    vector<int> dp(C + 1, 0);


    for (int i = 0; i < w.size(); ++i) {

        for (int r = 0; r < w[i]; ++r) {

            deque<int> q;

            for (int j = r; j <= C; j += w[i]) {

                // 维护队列头部不超过物品数量限制

                while (!q.empty() && q.front() < j - s[i] * w[i]) {

                    q.pop_front();

                }

                // 用偏移量计算当前候选值

                while (!q.empty() && dp[q.back()] + (j - q.back()) / w[i] * v[i] <= dp[j]) {

                    q.pop_back();

                }

                q.push_back(j);

                // 更新 dp[j]

                if (!q.empty()) {

                    dp[j] = max(dp[j], dp[q.front()] + (j - q.front()) / w[i] * v[i]);

                }

            }

        }

    }

    return dp[C];

}


int main() {

    int C = 10;

    vector<int> w = {2, 3, 4};

    vector<int> v = {3, 4, 5};

    vector<int> s = {3, 2, 1};


    int result = multipleKnapsackMonotonicQueue(C, w, v, s);

    cout << "最大价值(单调队列优化): " << result << endl;  // 输出 13

    return 0;

}

image.png

如何选择方法?

  • 二进制优化:代码简单,适合大多数场景(如 LeetCode、ACM 竞赛)。

  • 单调队列优化:效率更高,但实现复杂,适合对性能要求极高的场景(如 和 较大时)。