01背包
有 N件物品和一个容量是 V的背包。每件物品只能使用一次。
第 i件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。
输出最大价值。
输入格式
第一行两个整数,N,V
,用空格隔开,分别表示物品数量和背包容积。
接下来有 N行,每行两个整数 vi,wi,用空格隔开,分别表示第 i件物品的体积和价值。
输出格式
输出一个整数,表示最大价值。
数据范围
0<N,V≤1000
0<vi,wi≤1000
输入样例
4 5
1 2
2 4
3 4
4 5
输出样例:
8
解答
此思维导图来源(未标注来源的基本是自己画的,除非忘了):acwing:jasonlin
核心思想:从集合角度来分析DP问题
dp问题,在有限集合中求最值
为什么用dp?
因为集合中数量太多
dp为什么能解决数量太多的问题
用两个阶段,每次选出某种状态下最优解,不再穷举
一阶段状态表示
化零为整状态表示----不是一个元素一个元素枚举,每次枚举一类元素,化成子集,用一个状态来表示
状态表示f(i)分为:集合定位和集合属性
二阶段状态计算
状态计算,化整为零
例如需要求f(i),f(i)是最大值,然后我们只需要将f(i)划分成集合
判断每个子集的最大值,哪个更大,哪个就是所有集合最大值
子集一般满足两个原则(不遗漏,不重复),有时候可以重复,但一定不能遗漏
集合的划分依据:寻找最后一个不同点
dp问题很多种
选择dp,线性DP,区间DP
这题是选择DP
套用闫氏dp法
1.上回合的状态表示,使用状态计算,计算下回合的状态表示。
2.每步都选出最优解,通过上步最优解,算出下步最优解,从而降低时间复杂度,快速解决问题。
-----闫氏dp选择dp感悟总结
代码实现
#include<iostream>
#include<cmath>
using namespace std;
int u[1010],w[1010];
int f[1010][1010];
int main(){
int N,V;
cin>>N>>V;
//读入价值和重量,i从1开始,防止双层循环内i-1越界
for(int i=1;i<=N;i++)scanf("%d %d",&u[i],&w[i]);
//遍历每种状态表示下的集合属性(最大价值)
for(int i=1;i<=N;i++){//从1开始,前0个数无意义,而且会造成i-1越界
//容量从0开始,每次内层循环,算出前i个数在容量为V时的最大价值
//这个数是f[i][V],则f[N][V],是N个数在V容量内最大价值
for(int j=0;j<=V;j++){
//和前缀和有点像,前缀和保存的是和,f[i][j]保存的是最大价值
//在上轮外层循环时得出过f[i-1][j]的最大价值,直接取用
f[i][j]=f[i-1][j];
//先判断,避免目前容量无法存储i的体积
//max参数内的f[i][j]是f[i-1][j],然后和f[i-1][j-u[i]]+w[i]比较
//f[i-1][j-u[i]]是去掉i的体积后,选前i个数的最大价值选法的价值
//-u[i]是留出来添加i的容量,然后+w[i]即是加上i的价值
//f[i-1][j]和f[i-1][j-u[i]]+w[i]比较,大的那个就是f[i][j]的最大价值
if(j>=u[i])f[i][j]=max(f[i][j],f[i-1][j-u[i]]+w[i]);
}
}
//参照13,14行注释,输出结果
cout<<f[N][V];
return 0;
}
优化
思路
在时间复杂度上已经几乎无法优化,但是在空间复杂度上可以
把f[1010][1010]优化成一维数组,每次我们循环只需要f[i][*]和f[i-1][*]
不需要把所有f[i]都存下来
注释展示思路
#include<iostream>
#include<cmath>
using namespace std;
int u[1010],w[1010];
int f[1010][1010];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++)scanf("%d %d",&u[i],&w[i]);
for(int i=1;i<=N;i++){
for(int j=0;j<=V;j++)
{
//优化成一维,f[i][j]=f[i-1][j]就变成了f[j]=f[j]
//等号右边的是还没更新的f[j],这个f[j]是上次i循环里的
//那么对于这次i循环,它等价于f[i-1][j]
//优化为f[j]=f[j];
f[i][j]=f[i-1][j];
//下面的f[i][j]已经在上面证明过==f[i-1][j]
//所以只需要优化f[i-1][j-u[i]]
//f[j-u[i]]对应的值是f[i][j-u[i]],因为j-u[i]<j
//此时f[j-u[i]]已经被本次i循环遍历过了,所以是[i][j-u[i]]
//但是如果我们把内层循环改成从大到小遍历,f[j-u[i]]就是在f[j]后更新
//f[j-u[i]]就还未更新,他的值还是f[i-1][j-u[i]](上次i循环内的f[j-u[i]])
//所以我们把内循环优化成从大到小遍历即可
//因为本身不需要本次内循环的小j,推大j,只需要上次i-1的小j推大j
//所以对结果没影响
if(j>=u[i])f[i][j]=max(f[i][j],f[i-1][j-u[i]]+w[i]);
}
}
cout<<f[V];
return 0;
}
代码实现
#include<iostream>
#include<cmath>
using namespace std;
int u[1010],w[1010];
int f[1010];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++)scanf("%d %d",&u[i],&w[i]);
for(int i=1;i<=N;i++){
for(int j=V;j>=0;j--)
{
//优化成一维,f[i][j]=f[i-1][j]就变成了f[j]=f[j]
//等号右边的是还没更新的f[j],这个f[j]是上次i循环里的
//那么对于这次i循环,它等价于f[i-1][j]
//优化为f[j]=f[j];
f[j]=f[j];
//下面的f[i][j]已经在上面证明过==f[i-1][j]
//所以只需要优化f[i-1][j-u[i]]
//f[j-u[i]]对应的值是f[i][j-u[i]],因为j-u[i]<j
//此时f[j-u[i]]已经被本次i循环遍历过了,所以是[i][j-u[i]]
//但是如果我们把内层循环改成从大到小遍历,f[j-u[i]]就是在f[j]后更新
//f[j-u[i]]就还未更新,他的值还是f[i-1][j-u[i]](上次i循环内的f[j-u[i]])
//所以我们把内循环优化成从大到小遍历即可
//因为本身不需要本次内循环的小j,推大j,只需要上次i-1的小j推大j
//所以对结果没影响
if(j>=u[i])f[j]=max(f[j],f[j-u[i]]+w[i]);
}
}
cout<<f[V];
return 0;
}
还能优化一点代码长度
十七行f[j]=f[j];无效代码,不用写
27行if判断可以写入for循环
#include<iostream>
#include<cmath>
using namespace std;
int u[1010],w[1010];
int f[1010];
int main(){
int N,V;
cin>>N>>V;
for(int i=1;i<=N;i++)scanf("%d %d",&u[i],&w[i]);
for(int i=1;i<=N;i++)
for(int j=V;j-u[i]>=0;j--)
f[j]=max(f[j],f[j-u[i]]+w[i]);
cout<<f[V];
return 0;
}