题目描述
有 N件物品和一个容量是 V 的背包。每件物品只能使用一次。
第 i件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出 字典序最小的方案。这里的字典序是指:所选物品的编号所构成的序列。物品的编号范围是 1…N。
输入格式
第一行两个整数,N,V,用空格隔开,分别表示物品数量和背包容积。
接下来有 N 行,每行两个整数 vi,wi,用空格隔开,分别表示第 i 件物品的体积和价值。
输出格式
输出一行,包含若干个用空格隔开的整数,表示最优解中所选物品的编号序列,且该编号序列的字典序最小。
物品编号范围是 1…N。
数据范围
0<N,V≤1000
0<vi,wi≤1000
样例
4 5
1 2
2 4
3 4
4 6
1 4
算法1
题目要求输出字典序最小的解,假设存在一个包含第1个物品的最优解,为了确保字典序最小那么我们必然要选第一个。那么问题就转化成从2~N这些物品中找到最优解。之前的$f(i,j)$记录的都是前$i$个物品总容量为$j$的最优解,那么我们现在将$f(i,j)$定义为从第$i$个元素到最后一个元素总容量为$j$的最优解。接下来考虑状态转移:
$f(i,j) = max(f(i + 1,j),f(i+1,j-v[i])+w[i])$
两种情况,第一种是不选第$i$个物品,那么最优解等同于从第$i+1$个物品到最后一个元素总容量为$j$的最优解;第二种是选了第$i$个物品,那么最优解等于当前物品的价值$w[i]$加上从第$i+1$个物品到最后一个元素总容量为$j-v[i]$的最优解。
计算完状态表示后,考虑如何的到最小字典序的解。首先$f(1,m)$肯定是最大价值,那么我们便开始考虑能否选取第1个物品呢。
如果$f(1,m) =f(2,m-v[1])+w[1]$,说明选取了第1个物品可以得到最优解。
如果$f(1,m) = f(2,m)$,说明不选取第一个物品才能得到最优解。
如果$f(1,m)=f(2,m)=f(2,m-v[1])+w[1]$,说明选不选都可以得到最优解,但是为了考虑字典序最小,我们也需要选取该物品。
C++ 代码
#include<cstring>
#include<iostream>
#include<algorithm>
using namespace std;
const int N = 1010;
int f[N][N];
int v[N],w[N];
int n,m;
int main()
{
cin >> n >> m;
for(int i = 1 ; i <= n ; i++)
cin >> 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 cur_v = m;
for(int i = 1 ; i <= n ; i++)
{ //如果是最后一个元素,特判一下,防止越界即可
if(i == n && cur_v >= v[i])
{
printf("%d ",i);
break;
}
if(cur_v <= 0)
break;
//判断下标是否越界
if(cur_v - v[i]>=0 && f[i][cur_v] == f[i + 1][cur_v - v[i]] + w[i]){
printf("%d ",i);
cur_v = cur_v - v[i];//选了第i个物品,剩余容量就要减小。
}
}
return 0;
}
写一下最后如何输出方案:
1.要达到题目中所说的 字典序最小,我们就要从最终结果往前推,看我们每一步选择了哪个物品
对应到代码中 就是判断 f[i][cur_v]==f[i+1][cur_v-v[i]]+w[i] 以此来推断第i个物品选择了没有
2. 为什么要采取这样由结果反推的输出方式?
因为在dp的过程中,最优的方案是在不断变化的,对于
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]);
可能在这一一步中 f[i][j]的最优选择是选择了第i件物品的
但是在后续递推的过程中,这个方案可能就被淘汰掉
因此不能在dp的过程中输出最优的方案,而必须由结果反推。
3.回到我们最初的问题: 输出字典序最小的方案
传统01背包问题 是大的编号在后(res 的编号是f[n][m])
由结果反推我们并不能够得到字典序最小,因此我们才选择反转一下方向做dp
为什么字典序最小就要从最终结果往前推啊
因为从前往后推时, 如果序号2和序号3两个的最大值都是10,那么最后记录的最大值对应的序号就是3。而我们想要2.
谢谢谢谢!
nb
悟了
秒啊哥们
在输出方案的循环里的前两个判断都没有意义。
的确
拙见,不详细,够自己理解。
再次理解字典序最小导出需要dp从后往前:
这主要是由于我在回溯状态路径的时候,是只能从最终态回溯的。最终态对应两种,从前往后,最终态是f[n][m],从后往前是f[1][m]。
在讲选哪个最终态之前,我先介绍能实现最大价值的所有方案的理解。
方案的不同,选择的元素也不同。
假如是前者最终态,由于转移是从n-1转过来的,在回溯的时候也更偏向于选择编号更大的,(因为编号较小的可能没有被选择的机会 ,体积已经被编号较大的选择消耗了)。假如是后者最终态,倾向于给编号较小的物品空间来构成最优解。
欢迎提问~~
这样是不是就不能优化成一维了
一维会丢失状态,只能用二维
注意这里第一维是i+1,因此不能优化为一维
字典序1和4 和2和3有区别吗?
当然有区别
字典序从前向后依次比较,如果对应位置相等就向后递推,如果不相等就已分高下。
如果不倒著來,可選可不選則必不選,可以理解,先選小的字典會較小
但為何倒序時,可選可不選還是必選呢,假如必選不就代表選了編號較大的了
优点没懂为什么输入必须先输完再进行操作,哪怕是倒着输入f[i+1]也用不上i之前的数据啊
为啥可选可不选的时候一定要选??
保证字典序最小,我们就优先选择靠前的物品
前面不选就只能后面选了
哭了
调整dp讨论顺序,学到了
谢谢
另外大神的输出方案的代码里面第一个判断条件可以去掉
因为我们的数组开的大 所以对于第n次循环 必有cur_v=w[n]
f[n][cur_v]=f[n+1][cur_v]+w[n]=0+w[n]=w[n]
大佬请问一下为什么j要从0开始转移?orz
因为 f[i][j] = f[i+1][j] 当 j <= v[i] 的时候也需要手动更新
清清楚楚
写的真好
爱你爱你
写的真好
我认为作者最后的判断还需要优化一下,这一点很关键
作者很nice
Good!