AcWing 12. 背包问题求具体方案

AcWing 12. 背包问题求具体方案

一、题目描述

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

i 件物品的体积是 vi,价值是 wi

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

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

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

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

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

物品编号范围是 1N

数据范围
0<N,V1000
0<vi,wi1000

输入样例

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],如果想知道是怎么到达这个最大值状态的,需要从后向前枚举每个物品,如果剩余空间容量大于等于物品体积,就考查一下目前的最大值是不是由某个减去vi的状态转移而来,如果是,就输出这个物品。

联想一下上面的栗子:2,1都是可以做为答案的,当然从后向前来枚举,每一个遇到的是2而不是1,就是 默认第一个遇到的有效,直接把体积减掉,继续向前考虑下一个子问题。这样的策略,肯定是大号在前,小号在后啊~,这样所求的是 字典序最大的

所以我们应该反一下, 从后往前去遍历所有物品,这样f[1][m]就是最后答案,那么我们就 从前往后遍历就可以求具体方案,这样求的是字典序最小的。


之前的f(i,j)记录的都是前i个物品总容量为j的最优解,那么我们现在将f(i,j)定义为从第i个元素到最后一个元素总容量不超过j的最优解。接下来考虑状态转移:


f[i][j] = f[i + 1][j]; //从后向前是往左走,遇到的新元素是i,和已经走过的老元素i+1进行对比

if (j >= v[i]) f[i][j] = max(f[i][j], f[i + 1][j - v[i]] + w[i]);

两种情况,第一种是不选第i个物品,那么最优解等同于从第i+1个物品到最后一个元素总容量为j的最优解;第二种是选了第i个物品,那么最优解等于当前物品的价值w[i]加上从第i+1个物品到最后一个元素总容量为jv[i]的最优解。

计算完状态表示后,考虑如何的到最小字典序的解。首先f(1,m)肯定是最大价值,那么我们便开始考虑能否选取第1个物品呢。

如果f(1,m)=f(2,mv[1])+w[1],说明选取了第1个物品可以得到最优解。

如果f(1,m)=f(2,m),说明不选取第一个物品才能得到最优解。

如果f(1,m)=f(2,m)=f(2,mv[1])+w[1],说明选不选都可以得到最优解,但是为了考虑字典序最小,我们也需要选取该物品。

三、实现代码

#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;
}
posted @   糖豆爸爸  阅读(187)  评论(0编辑  收藏  举报
编辑推荐:
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 记一次.NET内存居高不下排查解决与启示
阅读排行:
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· Docker 太简单,K8s 太复杂?w7panel 让容器管理更轻松!
历史上的今天:
2013-12-24 小不点需要修改的地址
Live2D
点击右上角即可分享
微信分享提示