You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

150 lines
5.4 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

##[$AcWing$ $12$. 背包问题求具体方案 ](https://www.acwing.com/problem/content/description/12/)
### 一、题目描述
有 $N$ 件物品和一个容量是 $V$ 的背包。每件物品只能使用一次。
第 $i$ 件物品的体积是 $v_i$,价值是 $w_i$。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 **字典序最小的方案**。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 $1…N$。
**输入格式**
第一行两个整数,$NV$,用空格隔开,分别表示物品数量和背包容积。
接下来有 $N$ 行,每行两个整数 $v_i,w_i$,用空格隔开,分别表示第 $i$ 件物品的体积和价值。
**输出格式**
输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。
物品编号范围是 $1…N$。
**数据范围**
$0<N,V1000$
$0<v_i,w_i1000$
**输入样例**
```cpp {.line-numbers}
4 5
1 2
2 4
3 4
4 6
```
**输出样例**
```cpp {.line-numbers}
1 4
```
### 二、只能使用二维状态表示
因为 <font color='black' size=4><b>求具体的方案</b></font>,我们就 <font color='red' size=4><b>不能采取之前滚动数组优化版本的 $01$背包</b></font>,因为这样会损失一些具体方案.
### 三、如何确保字典序最小?
因为要求字典序最小,那么我们肯定采取贪心策略: <font color='blue' size=4><b>能选序号小的就选序号小的</b></font>
举个栗子,给定一个原始朴素版本的$01$背包数据:
```cpp {.line-numbers}
2 3
2 4
2 4
```
输出答案:
```c++
4
```
这个非常好理解吧:有两个物品,一个体积为$3$的背包,每个物品只能选择或不选择,问最终不超过背包体积上限$3$时,最大价值是多少?
在原始的版本中,是不强调序号的概念的,最终只要最大值正确就可以,不关心是从哪个序号过来的,比如本题,其实是可以选择$1$号物品获取到$4$个价值,当然也可以选择$2$号物品获取$4$个价值。
用下面的代码模拟跑一下:
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1010;
int n, m, v[N], w[N];
int f[N][N];
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) {
scanf("%d %d", &v[i], &w[i]);
for (int j = 0; 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]);
}
}
int j = m;
for (int i = n; i >= 1; i--)
if (j >= v[i] && f[i][j] == f[i - 1][j - v[i]] + w[i]) {
printf("%d ", i);
j -= v[i];
}
return 0;
}
```
输出的答案是:
```cpp {.line-numbers}
2
```
这是什么意思?就是只要选择了序号为$2$的物品就可以达到最大价值最大价值是4。
<font color='blue' size=4><b>$Q$:为什么代码输出的是$2$号,而不是$1$号呢?</b></font>
我们来研究一下这段代码:
```cpp {.line-numbers}
int j = m;
for (int i = n; i >= 1; i--)
if (j >= v[i] && f[i][j] == f[i - 1][j - v[i]] + w[i]) {
printf("%d ", i);
j -= v[i];
}
```
因为最终的最大值保存在$f[n][m]$,如果想知道是怎么到达这个最大值状态的,需要从后向前枚举每个物品,如果剩余空间容量大于等于物品体积,就考查一下目前的最大值是不是由某个减去$v_i$的状态转移而来,如果是,就输出这个物品。
联想一下上面的栗子:$2,1$都是可以做为答案的,当然从后向前来枚举,每一个遇到的是$2$而不是$1$,就是 <font color='blue' size=4><b>默认第一个遇到的有效</b></font>,直接把体积减掉,继续向前考虑下一个子问题。这样的策略,肯定是大号在前,小号在后啊~,这样所求的是 <font color='red' size=4><b>字典序最大的</b></font>。
所以我们应该反一下, <font color='blue' size=4><b>从后往前去遍历所有物品</b></font>,这样$f[1][m]$就是最后答案,那么我们就 <font color='red' size=4><b> 从前往后遍历就可以求具体方案,这样求的是字典序最小的。</b></font>
倒推 **状态转移路径** 的时候,只能在 **分叉转移** 的时候,即 **当前** 物品既可以 **选** 又可以 **不选** 时,优先 **选**
因此,我们本题的 **背包$DP$** 需要倒过来(从$N$递推到$1$)做,然后再 **从$1$倒推回$N$** 找出路径
这样在抉择时,如果出现 **分叉转移**,我们就优先 **选** 当前物品即可
### 三、实现代码
```cpp {.line-numbers}
#include <bits/stdc++.h>
using namespace std;
const int N = 1100;
int f[N][N];
int n, m;
int v[N], w[N];
int main() {
scanf("%d %d", &n, &m);
for (int i = 1; i <= n; i++) scanf("%d %d", &v[i], &w[i]);
for (int i = n; i >= 1; i--)
for (int j = 0; 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]);
}
int j = m;
for (int i = 1; i <= n; i++)
if (j >= v[i] && f[i][j] == (f[i + 1][j - v[i]] + w[i])) {
printf("%d ", i);
j -= v[i];
}
return 0;
}
```