前言
首先我们来了解一下什么是背包问题,其题目大意是对于一定容量的背包,给出一些物品,解出在容量内所能得到物品价值的最大值。背包问题其实是DP(动态规划)中的一类问题,学习背包问题可以让我们深刻的理解DP算法思想,无论是面试题、算法竞赛,DP都是至关重要的一类算法,今天我来为大家总结常见的01背包思路分析与问题分析。
01背包问题
有 N 件物品和一个容量是 V 的背包。每件物品只能装入一次。
第 i 件物品的体积是 vi,价值是 wi。
求解将哪些物品装入背包,可使这些物品的总体积不超过背包容量,且总价值最大。 输出最大价值。
算法思路分析
背包问题给人最大的疑惑就是,这件物品我是该选,还是不该选,存在必选的东西吗?对于这类选不选的问题常见的算法有三种:
- 贪心
- 暴力搜索
- DP
假算法:贪心
第一次接触到背包问题的人可能会往着贪心的方向去思考,但是很遗憾的是01背包问题并不能被贪心算法解决。对于贪心算法我们首先需要制定最优策略。对于这个问题给出三种可能的最优策略
- 价值优先 从剩余的物品中,选出可以装入背包的价值最大的物品,价值最大的物品首先被装入,然后是下一个价值最大的物品,如此继续下去,直到装满背包为止。反例:考虑n=2, w=[100,10,10], v =[20,15,15], c = 105。当利用价值贪婪准则时,获得的解为x= [ 1 , 0 , 0 ],这种方案的总价值为20。而最优解为[ 0 , 1 , 1 ],其总价值为30。
- 体积优先 从剩余的物品中,选择可装入背包的重量最小的物品。虽然这种规则对于前面的例子能产生最优解,但在一般情况下则不一定能得到最优解。考虑n= 2 ,w=[10,20], p=[5,100], c= 2 5。当利用重量贪婪策略时,获得的解为x =[1,0], 比最优解[ 0 , 1 ]要差
- 性价比(价值、体积)优先 按比值的降序来排序,从第一项开始装背包,然后是第二项,依次类推,尽可能的多放,直到装满背包。利用此策略试解n= 3 ,w=[20,15,15], p=[40,25,25], c=30 时的最优解。虽然按性价比非递(增)减的次序装入物品不能保证得到最优解,但它是一个直觉上近似的解。
暴力搜索
暴力搜索理论上是可行的,DFS与BFS都可以达到我们想要的效果,对于每件物品我们将选与不选都记录下来,选取价值最大的即可。但是搜索的算法复杂度是O()当物品数量大于12时,我们将无法在1s内解决问题。
DP
有一句话叫DP是对搜索的优美优化,对于这句话我是十分赞同的,一般在遇到问题可以用搜索来解决,但是不满足时间条件时,我们往往可以尝试使用DP来优化。对于DP问题我们需要确定的最重要的问题是分析状态以及状态该如何转移
状态
首先考虑状态的维度,背包问题是在n个物品中选取满足体积之和小于v的物品的最大价值.将其转化为二维状态表示,f[n][v]=max;那么对于一般状态f[i][j] 就代表在前i件物品中体积为j的最大价值
状态转移
在面临第i件物品是否选择时,对于i>1,f[i-1][j] 的值已经确定且根据定义是最优解那么只剩下两种情况 1.首先如果不选该件物品那么答案和前一种相等f[i][j]=f[i-1][j]; 2.如果选择该件物品,首先背包容量确定为 j 要选取该件物品状态将会由扣除该件物品的体积的状态转移即:f[i][j]=f[i-1][j-v[i]]+w[i]
将两者取max,就是f[i][j] 的最优解了。 算法复杂度O() 空间复杂度O()
#include<iostream>
using namespace std;
int f[1005][1005],v[1005],w[1005];// f[i][j], j体积下前i个物品的最大价值
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
{
for(int j=1;j<=m;j++)
{
if(j<v[i])f[i][j]=ans[i-1][j];//体积不够无法装下当前物品只能不选
else
{
f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i]);//负责取最大
}
}
}
cout<<f[n][m];
return 0;
}
一维优化
上面我们已经成功的解决了01背包的问题,但是能否更加更加优化呢,我们发现当前的f[i][j] 只取决于上一轮的状态,这是否意味着我们可以只用一维数组来记录状态f[i-1][j] 呢?显然是可以的。 此时我们用f[j] 来表示之前的f[i-1][j]。
for(int i = 1; i <= n; i++) //用外层循环来表示这是对于第几个物品的抉择
for(int j = m; j >= 0; j--)//逆序
{
if(j < v[i])
f[i][j] = f[i - 1][j]; // 优化前
f[j] = f[j]; // 优化后,该行自动成立,可省略。
else
f[i][j] = max(f[i - 1][j], f[i - 1][j - v[i]] + w[i]); // 优化前
f[j] = max(f[j], f[j - v[i]] + w[i]); // 优化后
}
这里注意到我们将内层循环逆序了,这里要注意是必须需要逆序,对于二维版本内层循环的顺序是无所谓的,二维情况下,状态f[i][j]是由上一轮i - 1的状态得来的,f[i][j]与f[i - 1][j]是独立的。而优化到一维后,如果我们还是正序,则有f[较小体积]更新到f[较大体积],则有可能本应该用第i-1轮的状态却用的是第i轮的状态,造成了污染。
完整代码,可以看到更加的简洁
#include<iostream>
using namespace std;
int v[1010],w[1010];
int f[1010];
int main()
{
int n,m;
cin>>n>>m;
for(int i=1;i<=n;i++)
cin>>v[i]>>w[i];
for(int i=1;i<=n;i++)
for(int j=m;j>=v[i];j--)
{
f[j]=max(f[j-v[i]]+w[i],f[j]);
}
cout<<f[m];
}