疯狂的背包问题(10) - 多重背包问题 III

多重背包模板题,数据极大需用单调队列优化,按体积余数分组,滑动窗口维护最优前驱状态,O(NV)。

OJ: luogu

题目 ID: U663797

难度:提高

标签:动态规划多重背包单调队列背包

日期: 2026-08-08 23:11

题意

NN 种物品和一个容量为 VV 的背包。第 ii 种物品最多有 sis_i 件,每件体积为 viv_i,价值为 wiw_i。求最大总价值。N103N \le 10^3V5×104V \le 5\times 10^4si104s_i \le 10^4

思路

一句话本质:按体积取模分组后,每组内转移在等差数列下标上进行,形成滑动窗口最大值问题,用单调队列做到 O(1)O(1) 转移。

二进制分组够快吗?

不够。每种物品拆出约 1414 个包,总物品数约 1.4×1041.4\times 10^4,跑 01 背包是 O(1.4×104V)7×108O(1.4\times 10^4 \cdot V) \approx 7\times 10^8,远大于题目限制。需要 O(NV)O(NV) 级别的做法。

直接看转移方程,dependence 是什么样的?

对于第 ii 种物品(体积 vv,价值 ww,数量 ss),dp[c]dp[c] 的候选前驱是 dp[cv],dp[c2v],,dp[csv]dp[c-v],dp[c-2v],\dots,dp[c-s\cdot v]。即它只依赖与 cc 相差 vv 的倍数的状态。

这个依赖结构提示什么?

0V0\sim V 的容量按除以 vv 的余数分成 vv 组。第 rr 组包含位置 r,r+v,r+2v,r,r+v,r+2v,\dots。不同余数的组之间完全独立——dp[r+kv]dp[r+kv] 的转移只用到同组的前驱 dp[r+(ks)v],,dp[r+(k1)v]dp[r+(k-s)v],\dots,dp[r+(k-1)v]

分完组后,转移变成什么形式?

令组内下标为 kk(位置 r+kvr+kv),只考虑前一轮 dp 值(记为 gg,防止同一轮内重复读取)。则:

dp[r+kv]=maxksjk(g[r+jv]+(kj)w) dp[r+kv] = \max_{k-s \le j \le k} \big(g[r+jv] + (k-j)w\big)

(kj)w(k-j)w 展开:=maxksjk(g[r+jv]jw)+kw= \max_{k-s \le j \le k} \big(g[r+jv] - jw\big) + kw

这个式子像什么?

对于 (g[r+jv]jw)(g[r+jv]-jw) 这一项,窗口 [ks,k][k-s,k]kk 增大而向右滑动——这是经典的滑动窗口最大值。窗口大小为 s+1s+1,每步 kk 前进 1,新元素 g[r+kv]kwg[r+kv]-kw 入队,旧的 ks1k-s-1 出队。

怎么 O(1)O(1) 取窗口最大值?

单调队列维护递减的 g[r+jv]jwg[r+jv]-jw 值。队首始终是窗口内最大值对应的下标 jj。每步先弹出越界的 j<ksj < k-s,再弹出队尾不够优的元素,最后用队首计算 dp[r+kv]=g[r+qheadv]+(kqhead)wdp[r+kv] = g[r+q_{\text{head}}\cdot v] + (k-q_{\text{head}})w

代码

cpp
/**
 * Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
 * rbook: -> https://rbook.roj.ac.cn  https://rbook2.roj.ac.cn
 * rainboy的学习导航网站: https://idx.roj.ac.cn
 * 多重背包问题 III — 单调队列优化
 */
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

const int maxn = 50005;
int n, V;
int dp[maxn], g[maxn];
int q[maxn], head, tail;

int main() {
    ios::sync_with_stdio(false); cin.tie(0);
    cin >> n >> V;
    for (int i = 1; i <= n; ++i) {
        int v, w, s;
        cin >> v >> w >> s;
        memcpy(g, dp, sizeof(dp));
        for (int r = 0; r < v; ++r) {
            head = 0, tail = 0;
            for (int k = 0; r + k * v <= V; ++k) {
                int idx = r + k * v;
                int val = g[idx] - k * w;
                while (head < tail && q[head] < k - s) ++head;
                while (head < tail && g[r + q[tail - 1] * v] - q[tail - 1] * w <= val) --tail;
                q[tail++] = k;
                dp[idx] = g[r + q[head] * v] + (k - q[head]) * w;
            }
        }
    }
    cout << dp[V] << '\n';
    return 0;
}

复杂度

  • 时间O(NV)O(NV)。每组余数内每个位置入队出队各一次,每件物品均摊线性于 VV
  • 空间O(V)O(V)(dp 数组 + 备份数组 g + 单调队列)。

总结

多重背包从 O(NVsi)O(NV\sum s_i)O(NVlogsi)O(NV\log s_i) 再到 O(NV)O(NV),三个版本层层递进。单调队列优化的关键洞察是:固定 vv 后转移在等差数列下标上进行,第 kk 个位置只依赖前 ss 个位置中"最值"的那个,转化为滑动窗口即水到渠成。