多重背包的二进制优化以及单调队列优化

题目概述

给定 \(n\) 个物品和一个容量为 \(m\) 的背包。第 \(i\) 个物品的体积、价值以及数量分别为: \(v_i\)\(w_i\)\(s_i\),求在总容量不超过 \(m\) 的前提下可获得的最大价值。

思路

\(f_{i,j}\) 表示考虑前 \(i\) 个物品、且在容量约束为 \(j\) 下的最大价值。

由于第 \(i\) 个物品可以选取 \(s_i\) 个,可以类似于为优化的完全背包,在转移时额外增加一层循环枚举选取数量不超过 \(s_i\) 的情况,其时间复杂度为\(O(nms)\),其中 \(s=\max\{s_1,\dots,s_n\}\)。当 \(n,m,s\) 同阶时,可写作:\(O(n^3)\)。下面直接讨论优化方式。

二进制优化

完全背包的优化方式不能直接应用于多重背包。因为完全背包允许无限取用某一物品,其状态转移所依赖的状态大于等于多重背包。下面通过一个简单例子说明这一差异:

  • 考虑 \(n=1,m=9,v_1=3,w_1=5,c_1=2\),可以发现,若为完全背包,最大价值为 15;而在多重背包中,最大价值仅为 10。

换句话说,多重背包在将第 \(i\) 件物品全部选取完后,仍然存在剩余容量,从而导致其性质与完全背包本质不同。

引理(二进制拆分原理)

\(S=2^0+2^1+\dots+2^{k-1}\),则集合 \(\{2^0,2^1,\dots,2^{k-1}\}\) 的元素可以通过加法表示区间 \([0,S]\) 内的所有整数。

推论

进一步证明推论:若 \(S=2^0+2^1+\dots+2^{k-1}+x, \ 1\le x<2^{k}\),则集合 \(\{2^0,2^1,\dots,2^{k-1},x\}\) 可以组合出 \([0,S]\) 内的所有整数。

设集合 \(Q=\{2^0,2^1,\dots,2^{k-1}\}\),并令 \(S'=2^0+2^1+\dots+2^{k-1}\)。由上述引理可知,区间 \([0,S']\) 可以由集合 \(Q\) 表示。于是只需证明区间 \([S',S]\) 可以由集合 \(P=Q\cup\{x\}\)表示。

对于任意 \(S'+n,1\le n\le x\),有

\[S'+n=(S'+n-x)+x \]

又因为 \(n\le x\Rightarrow S'+n-x\le S'\),所以 \(S'+n-x\) 可由集合 \(Q\) 表示,进而 \(S'+n\) 可由集合 \(P\) 表示,故命题成立。

回到多重背包问题,对于第 \(i\) 个物品的数量 \(s_i\),若可以将其拆分为若干份,使这些份数的能够表示区间 \([0,s_i]\) 内的任意整数。具体而言,将 \(s_i\) 按 2 的整数幂进行拆分。设 \(k\) 为满足 \(s_i\ge2^0+2^1+\dots+2^{k-1}\) 的最大整数,分类讨论 \(s_i\) 的两种情况:

  • \(s_i=2^0+2^1+\dots+2^{k-1}\),由二进制拆分定理,其可被转换为 01 背包问题
  • \(s_i=2^0+2^1+\dots+2^{k-1}+x,1\le x<2^{k}\),由上述推论,同样可以转换为 01 背包问题。

通过上述分析,多重背包问题可以通过二进制拆分完全转化为 01 背包问题,下面给出若干示例以帮助理解拆分过程:

  • \(6=1+2+3\)
  • \(8=1+2+4+1\)
  • \(18=1+2+4+3\)
  • \(31=1+2+4+8+16\)

二进制优化参考代码

#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int v[N], w[N];
int f[N];
int main() {
    int n, m, cnt = 0;
    cin >> n >> m;
    for (int i = 1; i <= n; i ++) {
        int a, b, s, k = 1;
        cin >> a >> b >> s;
        while (s >= k) {
            v[++ cnt] = k * a;
            w[cnt] = k * b;
            s -= k;
            k *= 2;
        }
        if (s) {
            v[++ cnt] = s * a;
            w[cnt] = s * b;
        }
    }
    n = cnt;
    for (int i = 1; i <= n; i ++) {
        for (int j = m; j >= v[i]; j --) {
            f[j] = max(f[j], f[j - v[i]] + w[i]);
        }
    }
    cout << f[m];
    return 0;
}

复杂度分析

  • 时间复杂度:\(O(m \sum \log s_i)\)。每个物品 \(s_i\) 被拆分为 \(O(\log s_i)\) 个新物品,每个新物品做一次 \(O(m)\) 的 01 背包转移。
  • 空间复杂度:\(O(m + \sum \log s_i)\)

单调队列优化

仔细观察状态转移表达式

\[f_{i,j} = \max\big( f_{i-1,j}, f_{i-1,j-v}+w, f_{i-1,j-2v}+2w, \dots, f_{i-1,j-sv}+sw \big) \]

其中第 \(i\) 件物品的体积为 \(v\),价值为 \(w\),数量上限为 \(s\)

不难发现,\(f_{i,j}\) 仅可能由满足 \(j\equiv k\pmod v,\ k\le j\)\(f_{i-1,k}\) 转移而来。换句话说,对于当前重量 \(j\) 而言,其只依赖于 \(i-1\) 阶段中 \(\bmod v\) 同余的更小的体积 \(k\)

举个例子:设 \(v=2,j=9\),其依赖的状态分别 \(f_{i-1,9},f_{i-1,7},f_{i-1,5},f_{i-1,3},f_{i-1,1}\),均满足\(k\equiv9\pmod2\)

因此,可以对所有可能的容量 \(j\) 按照模 \(v\) 分组,枚举余数 \(r=j\bmod v, \ r\in\{0,1,\dots,v-1\}\),对于每个余数 \(r\),枚举以 \(r\) 为起点,\(v\) 为步长的容量序列

\[r,r+v,r+2v,\dots,r+kv \]

其中 \(k\) 为满足 \(r+kv\le m\) 的最大整数。

固定 \(r\),设当前容量为 \(j=r+kv\),根据上述分析,其应从 \(i-1\) 阶段的 \([j-sv,j-(s-1)v, \dots,j-v,j]\) 中选择使得 \(f_{i,j}\) 最大的容量,这属于分组窗口最大值问题,可以用单调队列优化。

需要声明的是,这里队列存储的不是最大容量,而是使得后续状态最优的容量。

定义队列 \(q\) 维护 \(i-1\) 阶段在模 \(v\) 意义下的大小为 \(s + 1\) 的窗口 \([j-sv,j]\) 中,使得 \(f_{i,j}\) 最优的状态对应的容量,其中 \(h, t\) 分别表示 \(q\) 的头尾索引。

考虑如何维护队列最大值。设后续容量为 \(x\),则当

\[f_{i-1,j}+\frac{x-j}{v}\cdot w \ge f_{i-1,q_t}+\frac{x-q_t}{v} \cdot w \]

时,则说明 \(f_{i-1,j}\) 的作为 \(f_{i,x}\) 的转移状态比 \(f_{i-1,q_{t}}\) 更优,且更靠后,于是移除队尾。但由于 \(x\) 是未知量,于是将上述不等式的 \(x\) 右移,得到

\[f_{i,j}\ge f_{i,q_t}+\frac{j-q_t}{v}\cdot w \]

,用该不等式作为维护队列单调性的判断条件。

然后将 \(j\) 加入队列,并判断是否 \(q_{t}\) 在窗口 \([j - sv,j]\) 内部,即若 \(q_{h}<j-sv\),则将队头出队。

接着用 \(q_{h}\) 更新 \(f_{i,j}\),即

\[f_{i,j}=\max\left( f_{i,j}, f_{i-1,q_h}+\frac{j-q_h}{v}\cdot w \right) \]

参考代码 1

#include <bits/stdc++.h>
using namespace std;
const int N = 1010, M = 2000;
int f[N][M], q[M];

int main() {
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        for (int r = 0; r < v; r ++) {
            int h = 0, t = -1;
            for (int j = r; j <= m; j += v) {
                while (h <= t && f[i - 1][j] >= f[i - 1][q[t]] + (j - q[t]) / v * w) {
                    t --;
                }
                q[++ t] = j;
                if (h <= t && q[h] < j - s * v) h ++;
                f[i][j] = max(f[i][j], f[i - 1][q[h]] + (j - q[h]) /  v * w);
            }
        }
    }
    cout << f[n][m];
    return 0;
}

空间优化

不难发现,与 01 背包类似,\(i\) 阶段仅仅依赖于 \(i-1\) 阶段,但不同的是,由于多重背包是在枚举 \(i\) 阶段的在模 \(v\) 意义下的容量 \(j\) 的同时通过队列维护 \(i-1\) 阶段的在模 \(v\) 意义下的容量 \([j-sv,j]\),若倒序枚举,则无法用队列维护这个 \(k\)

于是我们考虑用一个数组 \(g\) 维护上一个阶段的容量,于是

\[f_j = \max\left( g_j, g_{q_h}+\frac{j-q_h}{v}\cdot w \right) \]

但需要注意的是,\(f_{j}\) 在未被更新前,本身表示的就是原 \(f_{i-1,j}\) 的状态,所以队列维护的窗口应该变为 \([j-sv,j-v]\)。而这只需要将维护队列最大值与更新状态进行互换即可。

参考代码 2

#include <bits/stdc++.h>
using namespace std;
const int N = 20010;
int f[N], g[N], q[N];

int main() {
    int n, m;
    cin >> n >> m;
    
    for (int i = 1; i <= n; i ++) {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, f, sizeof(g));
        for (int r = 0; r < v; r ++) {
            int h = 0, t = -1;
            for (int j = r; j <= m; j += v) {
                if (h <= t && q[h] < j - s * v) h ++;
                if (h <= t) f[j] = max(f[j], g[q[h]] + (j - q[h]) / v * w);
                while (h <= t && g[j] >= g[q[t]] + (j - q[t]) / v * w) t --;
                q[++ t] = j;
            }
        }
    }
    cout << f[m];
    return 0;
}

复杂度分析

  • 时间复杂度:\(O(nm)\)
  • 空间复杂度:\(O(m)\)
posted @ 2026-04-08 15:31  uvwijk  阅读(30)  评论(0)    收藏  举报