|
|
|
@ -1,4 +1,4 @@
|
|
|
|
|
## [AcWing 6. 多重背包问题 III](https://www.acwing.com/problem/content/6/)
|
|
|
|
|
## [$AcWing$ $6$. 多重背包问题 $III$](https://www.acwing.com/problem/content/6/)
|
|
|
|
|
|
|
|
|
|
### 一、题目描述
|
|
|
|
|
有 $N$ 种物品和一个容量是 $V$ 的背包。
|
|
|
|
@ -40,22 +40,8 @@ $0<v_i,w_i,s_i≤20000$
|
|
|
|
|
10
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 二、多重背包的前世今生
|
|
|
|
|
[$AcWing 4$. 多重背包问题 I](https://www.acwing.com/problem/content/4/)
|
|
|
|
|
[$AcWing 5$. 多重背包问题 II](https://www.acwing.com/problem/content/5/)
|
|
|
|
|
[$AcWing 6$. 多重背包问题 III](https://www.acwing.com/problem/content/6/)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 三、空间问题
|
|
|
|
|
下面将讨论此问题的三种解法,特别说明的是,二维最好理解,而且空间范围也是在可以接受的范围内,不必盲目追求一维,性能上不会带来提升。以最终极版本的单调队列优化算法来说,需要的二维空间最大值就是$f[N][M]$,其中$N*M=1000\times 20000=20000000$,换算成空间大小就是$$\large 1000\times 20000\times4/1024/1024=76MB$$,一般题目的空间限制都是$128MB$左右,再加上$C++$程序运行需要的一部分内存,是可以正常通过测试的,事实上二维方法,在[$AcWing$ $6$. 多重背包问题 $III$](https://www.acwing.com/problem/content/6/) 中,是可以正常$AC$的。
|
|
|
|
|
|
|
|
|
|
即使题目限制了内存大小最多为$64MB$(这就很$BT$了),也可以简单的使用滚动数组的方法优化,$$\large 2\times 20000\times4/1024/1024=16MB$$
|
|
|
|
|
|
|
|
|
|
足够过掉此题,一维限制无意义,也不做为讲解的重点,此文只关注二维实现,文末将附上一维实现办法。
|
|
|
|
|
|
|
|
|
|
### 四、三种解法
|
|
|
|
|
<font color='red' size=4><b>三种解法的根本区别在于数据范围,题面都是一样的:</b></font>
|
|
|
|
|
### 二、三种解法
|
|
|
|
|
三种解法的根本区别在于数据范围,题面都是一样的:
|
|
|
|
|
|
|
|
|
|
<!-- 让表格居中显示的风格 -->
|
|
|
|
|
<style>
|
|
|
|
@ -75,191 +61,10 @@ $0<v_i,w_i,s_i≤20000$
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<center><img src='https://cdn.acwing.com/media/article/image/2021/06/17/55909_ba412471cf-IMG_4AD8EC65CFE1-1.jpeg'></center>
|
|
|
|
|
|
|
|
|
|
* **状态表示**
|
|
|
|
|
集合:所有只从前$i$个物品中选,并且总体积不起过$j$的选法
|
|
|
|
|
属性:集合中每一个选法对应的总价值的最大值
|
|
|
|
|
|
|
|
|
|
* **状态计算**
|
|
|
|
|
就是一个集合划分的过程,就是和完全背包很像,但不像完全背包有无穷多个,而是有数量限制
|
|
|
|
|
|
|
|
|
|
* 初始状态:`f[0][0]`
|
|
|
|
|
* 目标状态:`f[n][m]`
|
|
|
|
|
|
|
|
|
|
#### 状态转移方程
|
|
|
|
|
$$\large f[i][j] = max\{(f[i-1][j − k*v[i]] + k*w[i]) | 0 ≤ k ≤ s[i],j>=k*v[i]\}$$
|
|
|
|
|
|
|
|
|
|
### 四、朴素算法
|
|
|
|
|
|
|
|
|
|
#### 二维朴素
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 110;
|
|
|
|
|
int n, m;
|
|
|
|
|
int f[N][N];
|
|
|
|
|
int main() {
|
|
|
|
|
scanf("%d %d", &n, &m);
|
|
|
|
|
for (int i = 1; i <= n; i++) {
|
|
|
|
|
int v, w, s;
|
|
|
|
|
scanf("%d %d %d", &v, &w, &s);
|
|
|
|
|
for (int j = 0; j <= m; j++)
|
|
|
|
|
for (int k = 0; k <= s && v * k <= j; k++)
|
|
|
|
|
f[i][j] = max(f[i][j], f[i - 1][j - k * v] + w * k);
|
|
|
|
|
}
|
|
|
|
|
printf("%d\n", f[n][m]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
#### 一维朴素
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 110;
|
|
|
|
|
int n, m;
|
|
|
|
|
int f[N];
|
|
|
|
|
int main() {
|
|
|
|
|
scanf("%d %d", &n, &m);
|
|
|
|
|
for (int i = 1; i <= n; i++) {
|
|
|
|
|
int v, w, s;
|
|
|
|
|
scanf("%d %d %d", &v, &w, &s);
|
|
|
|
|
for (int j = m; j >= v; j--)
|
|
|
|
|
//注意:此处k=0,k=1是一样的
|
|
|
|
|
//如果不要i物品 即 f[i][j]=f[i-1][j]
|
|
|
|
|
//转为一维表示法,就是f[j]=f[j],所以从0从1都一样
|
|
|
|
|
for (int k = 0; k <= s && k * v <= j; k++)
|
|
|
|
|
f[j] = max(f[j], f[j - v * k] + w * k);
|
|
|
|
|
}
|
|
|
|
|
printf("%d\n", f[m]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
在可以考虑第$i$个物品时,前面$i-1$个物品已经做出了选择,前面怎么选择的我不管,我只管我现在面临的情况该怎么处理:
|
|
|
|
|
|
|
|
|
|
$
|
|
|
|
|
\large \left\{\begin{array}{l}
|
|
|
|
|
第i个物品一个也不选择 & \\
|
|
|
|
|
第i个物品一个选1个& \\
|
|
|
|
|
第i个物品一个选2个& \\
|
|
|
|
|
... & \\
|
|
|
|
|
第i个物品一个选s_i个&
|
|
|
|
|
\end{array}\right.
|
|
|
|
|
$
|
|
|
|
|
当然,你也不能真的一定从$0$选择到$s_i$个,因为可能你的背包装不上了,需要加上限制条件:$v*k<=j$
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 五、二进制优化
|
|
|
|
|
朴素多重背包做法的本质:将有数量限制的相同物品看成多个不同的$0-1$背包。
|
|
|
|
|
|
|
|
|
|
优化的思路:比如我们从一个货车搬百事可乐的易拉罐(因为我爱喝不健康的快乐水~),如果存在$200$个易拉罐,小超市本次要的数量为一个小于$200$的数字$n$,搬的策略是什么呢?
|
|
|
|
|
|
|
|
|
|
A、一个一个搬,直到$n$为止。
|
|
|
|
|
|
|
|
|
|
B、在出厂前打成$64$个一箱,$32$个一箱,$16$个一箱,$8$个一箱,$4$个一箱,$2$个一箱,$1$个一箱,<font color='red'>**最后剩下的打成$73$个一箱**</font>。
|
|
|
|
|
为什么要把剩下的$73$个打成一个包呢?不是再分解成$64$,$32$这样的组合呢?这是因为我们其实本质是化解为$01$背包,一来这么分解速度最快,二来可以表示原来数量的任何子集,这样就$OK$了!
|
|
|
|
|
|
|
|
|
|
#### 二维进制版本
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 12010, M = 2010;
|
|
|
|
|
|
|
|
|
|
int n, m;
|
|
|
|
|
int v[N], w[N];
|
|
|
|
|
int f[N][M]; //二维数组版本,AcWing 5. 多重背包问题 II 内存限制是64MB
|
|
|
|
|
//只能通过滚动数组或者变形版本的一维数组,直接二维数组版本MLE
|
|
|
|
|
|
|
|
|
|
//多重背包的二进制优化
|
|
|
|
|
int main() {
|
|
|
|
|
scanf("%d %d", &n, &m);
|
|
|
|
|
|
|
|
|
|
int idx = 0;
|
|
|
|
|
for (int i = 1; i <= n; i++) {
|
|
|
|
|
int a, b, s;
|
|
|
|
|
scanf("%d %d %d", &a, &b, &s);
|
|
|
|
|
//二进制优化,能打包则打包之,1,2,4,8,16,...
|
|
|
|
|
int k = 1;
|
|
|
|
|
while (k <= s) {
|
|
|
|
|
idx++;
|
|
|
|
|
v[idx] = a * k;
|
|
|
|
|
w[idx] = b * k;
|
|
|
|
|
s -= k;
|
|
|
|
|
k *= 2;
|
|
|
|
|
}
|
|
|
|
|
//剩下的
|
|
|
|
|
if (s > 0) {
|
|
|
|
|
idx++;
|
|
|
|
|
v[idx] = a * s;
|
|
|
|
|
w[idx] = b * s;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
n = idx; //数量减少啦
|
|
|
|
|
// 01背包
|
|
|
|
|
for (int i = 1; i <= n; i++)
|
|
|
|
|
for (int j = 1; j <= m; j++) {
|
|
|
|
|
f[i][j] = f[i - 1][j];
|
|
|
|
|
if (j >= v[i]) f[i][j] = max(f[i][j], f[i - 1][j - v[i]] + w[i]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
printf("%d\n", f[n][m]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
#### 一维数组二进制版本
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
|
#include <bits/stdc++.h>
|
|
|
|
|
|
|
|
|
|
using namespace std;
|
|
|
|
|
const int N = 12010, M = 2010;
|
|
|
|
|
|
|
|
|
|
int n, m;
|
|
|
|
|
int v[N], w[N];
|
|
|
|
|
int f[M];
|
|
|
|
|
|
|
|
|
|
//多重背包的二进制优化
|
|
|
|
|
int main() {
|
|
|
|
|
scanf("%d %d", &n, &m);
|
|
|
|
|
|
|
|
|
|
int cnt = 0;
|
|
|
|
|
for (int i = 1; i <= n; i++) {
|
|
|
|
|
int a, b, s;
|
|
|
|
|
scanf("%d %d %d", &a, &b, &s);
|
|
|
|
|
//二进制优化,能打包则打包之,1,2,4,8,16,...
|
|
|
|
|
int k = 1;
|
|
|
|
|
while (k <= s) {
|
|
|
|
|
cnt++;
|
|
|
|
|
v[cnt] = a * k;
|
|
|
|
|
w[cnt] = b * k;
|
|
|
|
|
s -= k;
|
|
|
|
|
k *= 2;
|
|
|
|
|
}
|
|
|
|
|
//剩下的
|
|
|
|
|
if (s > 0) {
|
|
|
|
|
cnt++;
|
|
|
|
|
v[cnt] = a * s;
|
|
|
|
|
w[cnt] = b * s;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
n = cnt; //数量减少啦
|
|
|
|
|
// 01背包
|
|
|
|
|
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]);
|
|
|
|
|
|
|
|
|
|
printf("%d\n", f[m]);
|
|
|
|
|
return 0;
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
### 六、单调队列优化
|
|
|
|
|
### 三、单调队列优化
|
|
|
|
|
使用朴素版本利用数据进行调试,找一下规律,看看哪个状态间存在转移关系:
|
|
|
|
|
|
|
|
|
|
```cpp {.line-numbers}
|
|
|
|
@ -436,23 +241,23 @@ $f[i][j]$:前$i$个物品中选择,在体积上限是$j$的情况下,所
|
|
|
|
|
|
|
|
|
|
- 每一个二维表中的位置,都是可以从上一行中的某些位置转移而来的。比如:
|
|
|
|
|
|
|
|
|
|
$f[i-1][j] -> f[i][j]$
|
|
|
|
|
$f[i-1][j] \rightarrow f[i][j]$
|
|
|
|
|
|
|
|
|
|
$f[i-1][j-v]+w -> f[i][j]$
|
|
|
|
|
$f[i-1][j-v]+w \rightarrow f[i][j]$
|
|
|
|
|
|
|
|
|
|
$f[i-1][j-2v]+2w -> f[i][j]$
|
|
|
|
|
$f[i-1][j-2v]+2w \rightarrow f[i][j]$
|
|
|
|
|
|
|
|
|
|
$f[i-1][j-3v]+3w -> f[i][j]$
|
|
|
|
|
$f[i-1][j-3v]+3w \rightarrow f[i][j]$
|
|
|
|
|
|
|
|
|
|
....
|
|
|
|
|
|
|
|
|
|
$f[i-1][j-s*v]+s*w -> f[i][j]$
|
|
|
|
|
$f[i-1][j-s*v]+s*w \rightarrow f[i][j]$
|
|
|
|
|
|
|
|
|
|
当然,这也不一定都对,因为要保证$j-s*v>=0$
|
|
|
|
|
|
|
|
|
|
这些数据依赖是 **跳跃性的前序依赖**,所以,我们按对体积取模的余数分组,按组讨论,就可以把二维表填充满。
|
|
|
|
|
|
|
|
|
|
- 它的前序依赖单元格个数是$s$(指最大值)个,我们需要在这些个值中找出一个$max$。这是一个 距离我最近$X$个元素内找出最大值的典型问题:单调递减队列求区间最大值,队头元素即答案。
|
|
|
|
|
- 它的前序依赖单元格个数是$s$(指最大值)个,我们需要在这些个值中找出一个$max$。这是一个距离我最近$X$个元素内找出最大值的典型问题:单调递减队列求区间最大值,队头元素即答案。
|
|
|
|
|
|
|
|
|
|
- **$Q$:为什么是单调队列呢?如何运用单调队列求解呢?**
|
|
|
|
|
就是维护一个队列,它是由大到小的顺序单调存在的。对于后面每一个加入进来的数据,因为它是最新出生的,就算是最小,当前面老家伙们死光后,它也可能成为掌门人(黄鼠狼下豆鼠子,一辈不如一辈,这种情况就是可能的~),它必须保留!而它前面的老家伙,即使再厉害,由于年龄到了,也需要去世。没有来的及去世的老家伙们,因为能力值小于最后加入的数据,也就没有存在下去的必要,因为后面向前找,肯定先找到新出生而且能力值高的嘛,这些老家伙去世算了。
|
|
|
|
|