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.

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. 背包问题求具体方案

一、题目描述

N 件物品和一个容量是 V 的背包。每件物品只能使用一次。

i 件物品的体积是 v_i,价值是 w_i

求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。

输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N

输入格式 第一行两个整数,NV,用空格隔开,分别表示物品数量和背包容积。

接下来有 N 行,每行两个整数 v_i,w_i,用空格隔开,分别表示第 i 件物品的体积和价值。

输出格式 输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。

物品编号范围是 1…N

数据范围 0<N,V≤1000 0<v_i,w_i≤1000

输入样例

4 5
1 2
2 4
3 4
4 6

输出样例

1 4

二、只能使用二维状态表示

因为 求具体的方案,我们就 不能采取之前滚动数组优化版本的 01背包,因为这样会损失一些具体方案.

三、如何确保字典序最小?

因为要求字典序最小,那么我们肯定采取贪心策略: 能选序号小的就选序号小的

举个栗子,给定一个原始朴素版本的01背包数据:

2 3

2 4 
2 4

输出答案:

4

这个非常好理解吧:有两个物品,一个体积为3的背包,每个物品只能选择或不选择,问最终不超过背包体积上限3时,最大价值是多少?

在原始的版本中,是不强调序号的概念的,最终只要最大值正确就可以,不关心是从哪个序号过来的,比如本题,其实是可以选择1号物品获取到4个价值,当然也可以选择2号物品获取4个价值。

用下面的代码模拟跑一下:

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

输出的答案是:

2

这是什么意思?就是只要选择了序号为2的物品就可以达到最大价值最大价值是4。

Q:为什么代码输出的是2号,而不是1号呢?

我们来研究一下这段代码:

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 找出路径

这样在抉择时,如果出现 分叉转移,我们就优先 当前物品即可

三、实现代码

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