AcWing
  • 首页
  • 活动
  • 题库
  • 竞赛
  • 校园
  • 应用
  • 文章
    • 题解
    • 分享
    • 问答
  • 吐槽
  • 登录/注册

AcWing 6. 多重背包问题 III 详解 + yxc大佬代码解读    原题链接    困难

作者: 作者的头像   lys ,  2019-11-24 15:56:05 ,  所有人可见 ,  阅读 14193


200


104

题目描述

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;
}

76 评论


用户头像
活在梦里嘛   11天前     回复

dp[k] = max(dp[k], pre[q[head]] + (k - q[head])/v * w) 其实这个max可以去掉,因为是等价类划分的话不会重复计算,也就是说f[k[只会被算一次

用户头像
狂气电波   1天前     回复

好像是因为这个代码里的deque当前项是计算完max后才加的,计算max之前没有当前项


用户头像
Ramanujan_2   21天前     回复

q[++tail] = k;

这里入队的不是k吗?为什么是:
这样,每次入队的值是 dp[j+kv] - kw??

用户头像
Ramanujan_2   21天前     回复

感觉描述和代码实现有点矛盾


用户头像
perfectionist   1个月前     回复

大佬牛逼


用户头像
少年游_   1个月前     回复

还好看到了大佬的解释,清晰明了,tql


用户头像
範錦昌   1个月前     回复

代码head和tail看得不是很懂…

用户头像
quark   1个月前     回复

可以去看滑动窗口的题解,那里又关于单调队列的详细过程


用户头像
靓--_--仔   1个月前     回复

Orz


用户头像
Kazuya   1个月前     回复

tql


用户头像
Venarys   1个月前     回复

tql


用户头像
四盏电灯   2个月前     回复

6666tql


用户头像
RiverL   2个月前     回复

tql


用户头像
DPH   2个月前     回复

猛啊,太强了


用户头像
晓山青   3个月前     回复

tql


用户头像
shark_big   3个月前     回复

解释很懂,但是代码就懵了


用户头像
Mr.D   3个月前     回复

tql tql tql tql tql tql tql


用户头像
晒干了沉默   4个月前     回复

救救救救救救救救救救救救救救救救就就就这就这就这就这就这


用户头像
许多多   5个月前     回复

看懂了,舒服了。


用户头像
acwing_48959   5个月前     回复

if (head <= tail && k - sv > q[head])
++head;
这步不太理解,刚开始循环应该是k - s
v负的,我想应该是刚开始一直是入队,然后k - s*v变成正的,超过了si个物品界限,这时就需要出队得出最大值,这样q[head]直接0代替更直观,用q[head]让人以为有特殊含义。唉,思想倒是容易看,代码真不是人容易看的。

用户头像
acwing_48959   5个月前     回复

差点忘了,q[head]应该存的是余数,不能替换。代码细节不容易看懂


用户头像
Edward2019   6个月前     回复

CAO,有个地方写错了


用户头像
ncust202101   8个月前     回复

太棒了,视频里半知半解在这里一下点通了,主要是那几个转换方程一目了然


用户头像
Ninja@珏   9个月前     回复

接下来,我们把 dp[0] –> dp[m] 写成下面这种形式
这句话是不是跳过了思考过程啊,感觉突然冒出来一堆式子

用户头像
哎呀呀   8个月前     回复

怎么理解?没看懂

用户头像
Ninja@珏   8个月前    回复了 哎呀呀 的评论     回复

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]

怎么从上面那个式子,写出下面这个式子

用户头像
shawnyang   8个月前    回复了 Ninja@珏 的评论     回复

是对每个物品的体积进行了分类,物品体积 j 模上背包体积 v ,可以分成余数是0,1,2,3,…,v-1的v类

用户头像
aw7   4个月前    回复了 shawnyang 的评论     回复

谢谢

用户头像
与风做友   1个月前    回复了 shawnyang 的评论     回复

您好像说反了,是目前背包的体积模上物体体积v,把背包体积分为模v等于0 – (v-1)份

用户头像
小余   1个月前    回复了 Ninja@珏 的评论     回复

逆向思考, 在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


你确定删除吗?
1024
x

© 2018-2022 AcWing 版权所有  |  京ICP备17053197号-1
用户协议  |  常见问题  |  联系我们
AcWing
请输入登录信息
更多登录方式: 微信图标 qq图标
请输入绑定的邮箱地址
请输入注册信息