一、题目分析

单调队列优化 \(O(NV)\)

回顾一下朴素版本的\(dp\)方程:

\(f[i][j]\) 表示将前 \(i\) 种物品放入容量为 \(j\) 的背包中所得到的最大价值

\(f[i][j]\) = \(max\)(不放入物品 \(i\),放入\(1\)个物品 \(i\),放入\(2\)个物品 \(i\), ... , 放入\(k\)个物品 \(i\))

这里 \(k\) 要满足:\(k <= s\), \(j - k*v >= 0\)

不放物品 \(i\) = \(f[i-1][j]\)

放\(k\)个物品 \(i\) = \(f[i-1][j - k*v] + k*w\)

写完整就是:

\(f[i][j] = max(f[i-1][j], f[i-1][j-v] + w, f[i-1][j-2*v] + 2*w,..., f[i-1][j-k*v] + k*w)\)

因为第一个\(i\)加入后,都只对前面一个\(i-1\)依赖,可以重复利用\(f\)数组来保存上一轮的信息:

令 \(f[j]\) 表示容量为\(j\)的情况下,获得的最大价值,则针对每一类物品\(i\) ,我们都更新一下 \(f[m] --> f[0]\) 的值,最后 \(f[m]\) 就是一个全局最优值

\(f[m] = max(f[m], f[m-v] + w, f[m-2*v] + 2*w, f[m-3*v] + 3*w, ...)\)

接下来,我们把 \(f[0]\) --> \(f[m]\) 写成下面这种形式

\(f[0], f[v],f[2*v],f[3*v], ... , f[k*v]\) 余数是\(0\)的一组

\(f[1], f[v+1], f[2*v+1], f[3*v+1], ... , f[k*v+1]\) 余数是\(1\)的一组

\(f[2], f[v+2], f[2*v+2], f[3*v+2], ... , f[k*v+2]\) 余数是\(2\)的一组

...

\(f[j], f[v+j], f[2*v+j], f[3*v+j], ... , f[k*v+j]\) 余数是\(j\)的一组

显而易见,\(m\) 一定等于 \(k*v + j\),其中 \(0 <= j < v\)

所以,我们可以把 \(f\) 数组分成 \(j\) 个类,每一类中的值,都是在同类之间转换得到的,也就是说,\(f[k*v+j]\) 只依赖于 \({f[j], f[v+j], f[2*v+j], f[3*v+j], ... , f[k*v+j]}\)

因为我们需要的是\({f[j], f[v+j], f[2*v+j], f[3*v+j], ... , f[k*v+j] }\) 中的最大值,注意:长度是固定的k个!要求的是固定长度的区间最大值!

可以通过维护一个单调队列来得到结果。这样的话,问题就变成了 \(j\) 个单调队列的问题

所以,我们可以得到

\(f[j] = f[j]\)

\(f[j+v] = max(f[j] + w, f[j+v])\)

\(f[j+2v] = max(f[j] + 2w, f[j+v] + w, f[j+2v])\)

\(f[j+3v] = max(f[j] + 3w, f[j+v] + 2w, f[j+2v] + w, f[j+3v])\)

...

但是,这个队列中前面的数,每次都会增加一个 \(w\) ,所以我们需要做一些转换

\(f[j] = f[j]\)

\(f[j+v] = max(f[j], f[j+v] - w) + w\)

\(f[j+2v] = max(f[j], f[j+v] - w, f[j+2v] - 2w) + 2w\)

\(f[j+3v] = max(f[j], f[j+v] - w, f[j+2v] - 2w, f[j+3v] - 3w) + 3w\)

...

这样,每次入队的值是 \(f[j+k*v] - k*w\)

单调队列问题,最重要的两点

1)维护队列元素的个数,如果不能继续入队(队首离现在的差将要大于s),弹出队头元素

2)维护队列的单调性,即:尾值 \(>= dp[j + k*v] - k*w\)

本题中,队列中元素的个数应该为 \(s+1\) 个,即 \(0 -- s\) 个物品 \(i\)

二、核心问题

\(Q\):为什么可以使用单调队列来优化呢?

(1)\(f[j]\)状态只能从前面\(i-1\)种物品的\(j,j-v,j-2v,...,j-sv\)空间内获得,这是一个长度为\(s+1\),公差为\(v\)的等差数列

(2)按\(j%v\)的余数进行分组讨论,组内的数据前后存在依赖,组间的彼此之间不干扰。

(3)\(f[i,j]\)只关心与其相近的\(s+1\)个结果数据的最大值。

(4)随着\(j\)的长大,上面考查的区间也在向右移动,这就是一个标准的滑动窗口求最大值问题,所以可以使用单调队列来优化,以达到\(O(n)\)的时间复杂度,不必每次都从头找到尾了。

\(Q\):为什么最终答案是\(f[m]\), 每一类都互不相关,最终不应该枚举\(f[1~m]\)的最大值吗?

A:和普通的滑动窗口不一样,这里每次计算的\(f\)数组都是上次\(dp\)的结果。

可以理解为将余数为\(0\)的最优解算完后,做为结果参与了余数为\(1\)的结果最大值评选,然后每轮余数最优解都是这个概念,\(f[m]\)就是理所当然的最终解。

三、实现代码

#include <bits/stdc++.h>

using namespace std;
const int N = 20010;
typedef pair<int,int> PII;
PII q[N]; //单调队列数组
// 第一维:背包能装下多少个i物品 k=[0~m/v],用来控制滑动窗口的长度
// 第二维:真正滑动窗口中数据的值(单调队列)
int f[N]; //dp数组

int main() {
//优化输入
ios::sync_with_stdio(false);
int n, m;
cin >> n >> m;
//n类物品逐个加入讨论
for (int i = 1; i <= n; i++) {
int v, w, s;//体积,价值,个数
cin >> v >> w >> s;
//按余数j进行分类讨论
for (int j = 0; j < v; j++) {
int hh = 0, tt = -1; //初始化单调队列
for (int k = 0; k <= m / v; k++) { //枚举每个可能数量的物品i
//这个x是准备入队列的数据,通过公式变形得到此形态
int x = f[k * v + j] - k * w; //在放入k个物品i的情况下,可以获取到的最优解
//单调队列中比x小的出队
while (hh <= tt && x >= q[tt].second) tt--;
//x存储到单调队列中
q[++tt].first = k, q[tt].second = x;
//维护队列头,k-s表示滑动窗口的长度
if (q[hh].first < k - s) hh++;
//f[i-1]--->f[i]
f[k * v + j] = q[hh].second + k * w;
}
}
}
//输出
printf("%d\n", f[m]);
}