题目描述
https://www.acwing.com/problem/content/6/
算法1
(单调队列优化) $O(NV)$
一共 n 类物品,背包的容量是 m
每类物品的体积为v, 价值为w,个数为s
我们先来回顾一下传统的dp方程
dp[i][j] 表示将前 i 种物品放入容量为 j 的背包中所得到的最大价值
dp[i][j] = max(不放入物品 i,放入1个物品 i,放入2个物品 i, ... , 放入k个物品 i)
这里 k 要满足:k <= s, j - k*v >= 0
不放物品 i = dp[i-1][j]
放k个物品 i = dp[i-1][j - k*v] + k*w
dp[i][j] = max(dp[i-1][j], dp[i-1][j-v] + w, dp[i-1][j-2*v] + 2*w,..., dp[i-1][j-k*v] + k*w)
实际上我们并不需要二维的dp数组,适当的调整循环条件,我们可以重复利用dp数组来保存上一轮的信息
我们令 dp[j] 表示容量为j的情况下,获得的最大价值
那么,针对每一类物品 i ,我们都更新一下 dp[m] --> dp[0] 的值,最后 dp[m] 就是一个全局最优值
dp[m] = max(dp[m], dp[m-v] + w, dp[m-2*v] + 2*w, dp[m-3*v] + 3*w, ...)
接下来,我们把 dp[0] --> dp[m] 写成下面这种形式
dp[0], dp[v], dp[2*v], dp[3*v], ... , dp[k*v]
dp[1], dp[v+1], dp[2*v+1], dp[3*v+1], ... , dp[k*v+1]
dp[2], dp[v+2], dp[2*v+2], dp[3*v+2], ... , dp[k*v+2]
...
dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j]
显而易见,m 一定等于 k*v + j,其中 0 <= j < v
所以,我们可以把 dp 数组分成 j 个类,每一类中的值,都是在同类之间转换得到的
也就是说,dp[k*v+j] 只依赖于 { dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] }
因为我们需要的是{ dp[j], dp[v+j], dp[2*v+j], dp[3*v+j], ... , dp[k*v+j] } 中的最大值,
可以通过维护一个单调队列来得到结果。这样的话,问题就变成了 j 个单调队列的问题
所以,我们可以得到
dp[j] = dp[j]
dp[j+v] = max(dp[j] + w, dp[j+v])
dp[j+2v] = max(dp[j] + 2w, dp[j+v] + w, dp[j+2v])
dp[j+3v] = max(dp[j] + 3w, dp[j+v] + 2w, dp[j+2v] + w, dp[j+3v])
...
但是,这个队列中前面的数,每次都会增加一个 w ,所以我们需要做一些转换
dp[j] = dp[j]
dp[j+v] = max(dp[j], dp[j+v] - w) + w
dp[j+2v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w) + 2w
dp[j+3v] = max(dp[j], dp[j+v] - w, dp[j+2v] - 2w, dp[j+3v] - 3w) + 3w
...
这样,每次入队的值是 dp[j+k*v] - k*w
单调队列问题,最重要的两点
1)维护队列元素的个数,如果不能继续入队,弹出队头元素
2)维护队列的单调性,即:尾值 >= dp[j + k*v] - k*w
本题中,队列中元素的个数应该为 s+1 个,即 0 -- s 个物品 i
C++ 代码
#include <iostream>
#include <cstring>
using namespace std;
const int N = 20010;
int dp[N], pre[N], q[N];
int n, m;
int main() {
cin >> n >> m;
for (int i = 0; i < n; ++i) {
memcpy(pre, dp, sizeof(dp));
int v, w, s;
cin >> v >> w >> s;
for (int j = 0; j < v; ++j) {
int head = 0, tail = -1;
for (int k = j; k <= m; k += v) {
if (head <= tail && k - s*v > q[head])
++head;
while (head <= tail && pre[q[tail]] - (q[tail] - j)/v * w <= pre[k] - (k - j)/v * w)
--tail;
if (head <= tail)
dp[k] = max(dp[k], pre[q[head]] + (k - q[head])/v * w);
q[++tail] = k;
}
}
}
cout << dp[m] << endl;
return 0;
}
dp[k] = max(dp[k], pre[q[head]] + (k - q[head])/v * w) 其实这个max可以去掉,因为是等价类划分的话不会重复计算,也就是说f[k[只会被算一次
好像是因为这个代码里的deque当前项是计算完max后才加的,计算max之前没有当前项
q[++tail] = k;
这里入队的不是k吗?为什么是:
这样,每次入队的值是 dp[j+kv] - kw??
感觉描述和代码实现有点矛盾
大佬牛逼
还好看到了大佬的解释,清晰明了,tql
代码head和tail看得不是很懂…
可以去看滑动窗口的题解,那里又关于单调队列的详细过程
Orz
tql
tql
6666tql
tql
猛啊,太强了
tql
解释很懂,但是代码就懵了
tql tql tql tql tql tql tql
救救救救救救救救救救救救救救救救就就就这就这就这就这就这
看懂了,舒服了。
if (head <= tail && k - sv > q[head])
++head;
这步不太理解,刚开始循环应该是k - sv负的,我想应该是刚开始一直是入队,然后k - s*v变成正的,超过了si个物品界限,这时就需要出队得出最大值,这样q[head]直接0代替更直观,用q[head]让人以为有特殊含义。唉,思想倒是容易看,代码真不是人容易看的。
差点忘了,q[head]应该存的是余数,不能替换。代码细节不容易看懂
CAO,有个地方写错了太棒了,视频里半知半解在这里一下点通了,主要是那几个转换方程一目了然
接下来,我们把 dp[0] –> dp[m] 写成下面这种形式
这句话是不是跳过了思考过程啊,感觉突然冒出来一堆式子
怎么理解?没看懂
dp[m] = max(dp[m], dp[m-v] + w, dp[m-2v] + 2w, dp[m-3v] + 3w, …)
接下来,我们把 dp[0] –> dp[m] 写成下面这种形式
dp[0], dp[v], dp[2v], dp[3v], … , dp[k*v]
怎么从上面那个式子,写出下面这个式子
是对每个物品的体积进行了分类,物品体积 j 模上背包体积 v ,可以分成余数是0,1,2,3,…,v-1的v类
谢谢
您好像说反了,是目前背包的体积模上物体体积v,把背包体积分为模v等于0 – (v-1)份
逆向思考, 在j容量下的状态由(j), (j-v[i]), (j-2v[i]), (j-3v[i]) … 这些状态转移过来, 反过来想就是在(j)这一容量下, 不选物品, 在(j-v[i])容量下选择一件物品, 在(j-2v[i])容量下选择两件物品… 然后转移到(j)这个状态, 也就相当于是在初始容量为(i)的情况下, 不选, 选一件, 选两件, 三件… 然后转移到(i), (i+v[i]), (i+2v[i])… 这些状态。 理解了这个思维, 我们就能理解上面的式子, 但是怎么能刚好满足选完这些物品能到达m这一容量呢, 那么我们就得需要一个初始容量j, 使得 j+kv[i] == m, 划分出那么多个类, 只是为了推一下而已, 表达的意思就是总有那么一个j能满足 j+k*v[i] == m