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