😏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
题目分析
从题目中我们可以,知道这是一个类似于贪心的问题,但是有并不是那么贪心。
首先,我们想要让背包内的价值最大,那么,就需要每回都要拿最大的放进去,也就是这样一个思想:
想要让最后总体最大,我们就要让他们在每一个情况下取最大,这样就能实现在最后结果达到最大预期
但是,这时候,题目的第二个约束就浮现出来了,就是背包总共就那么大,我们要在有限的空间情况下具体考虑。这样一个过程是动态的,那么我们就是要在动态情况下进行空间或者是某些东西的使用,让预期符合结果。
就如同我们我们在打网游的背包一样,物品格就只有那么多,我们总归是要装一些价值高的东西进去,那些低价值的东西自然要抛弃掉了。
解法1(二维数组模拟背包)(动态规划)
通过上边的刨析我们很容易得到,其中一个思路就是用一个很大的二维数组来对背包进行模拟然后用循环的过程对装东西的逻辑进行实现。
但是我们如果是根据体积对背包进行格子的具象化的话,那样倒是并不好进行划分,我们不妨换一个思路,就是把原来意义是x,y坐标的下标,我们可以改成,将横向的下标用来表示,我们现在遍历到了第几个物品,然后,第二个坐标我们又来表示,我们现在的体积是多大,然后把当前状态下的最大值放到数组中存起来,就可以了。
其实,就相当于,你有一个不断变化的背包,你要把当前容量的,而且还有一个跟着变化的,选择列表,也就是相当于,我们把原来的只是单一的模拟背包格子,转变思路,把两个限制作为横纵坐标,进行枚举状态,然后从前边的状态得到当前状态的最优解,进行记录,相当于状态的转移
当前待选的元素个数\容积大小 | 0……m | …… |
---|---|---|
0……n | 0……arr**[n][m]** | |
…… | …… |
因为我们唯一确认的情况就是(0,0)情况所以我们对(0,0)的元素进行初始化就行,然后从**[i][0 ~ m]**进行推算,相当于是从,最开始的确定情况,一点一点递推到,我们预期的情况。
既然知道了,最终答案是由每个状态得最优选产生得,那下一步就要算出来每个状态的最优解是什么??,怎么算呢??
现在我们假设,已经选到了**arr[i] [j]**了我们下一步就有两个选择
- 就是选第i个物品,既然选了第i个物品,我们的背包总价值就变成了两部分,一个是不变量就是我们选取的第i件物品,另一个部分就是在**j - v[i]**体积下的,i - 1件物品的选择。
代码思路实现
朴素二维数组状态动态规划
#include<iostream>
#include<algorithm>
#include<cstring>
//这几个是我比较常用的几个头文件,写算阀体的时候我就习惯带上
using namespace std;
//因为数据范围是都小于1000,所以我们可以开一个大的数组进行存储,和相应的状态递推
//因为c++特性是可以用常变量来对数组进行开辟,为增加代码的可读性,我们设置常变量
const int N = 1010;//定义1010是为了防止我们用到1000下标
//状态转移的数组
int arr[N][N];
//定义价值还有体积的数组
int w[N],v[N];
int n,m;
int main()
{
cin >> n >> m;
//因为定义在全局就不需要对其进行初始化了
//读入数据
for(int i = 1;i <= n;i++)
{
cin >> v[i] >> w[i];
}
//枚举情况
for(int i = 1;i <= n;i++)
{
//j要从0开始进行枚举,因为容积是0也是合法的证明我们不选任何东西
for(int j = 0;j <= m;j++ )
{
//第一种情况
//不选第i个
arr[i][j] = arr[i - 1][j];
//如果容量充足,就选,然后和没选的取max
if(j >= v[i])//一定要记住当容积相等的时候也可以放所以要加上这种情况
{
arr[i][j] = max(arr[i][j],arr[i - 1][j - v[i]] + w[i]);
}
}
}
cout << arr[n][m];
return 0;
}
一维优化
我们通过上面的分析,我们不难发现,其实每次推算的数据用的就是上一层的数据和之前的数据其实就是我们在每次运算的时候,会用到arr[i - 1][j]以及arr[i - 1][j - v[j]]
,所以我们不妨将每一层的维度压缩到,统一维度。
状态的变化(i记录到了第几层的数据,用i进行控制) | 具体每一行的数据 |
---|---|
1~n(并不作为数组元素而是在外边循环控制层数) | 就是当前层的数据最大 |
但是我们压缩完了之后,把眼光放到循环语句处,我们会发现完蛋,有问题了。当我们进行到了i层的时候,想要用i - 1层的数据的时候,数据已经被覆盖了,因为我们采取的是如下策略:
所以我们不妨对数组倒着进行遍历,这样就不会被覆盖了。也能正常使用了。而且我们代码中的一些细节也可以进行优化了。
其实针对一维数组的优化,只不过是对代码的等价变换。
#include<iostream>
#include<algorithm>
#include<cstring>
using namespace std;
const int N = 1010;
int arr[N];
int w[N],v[N];
int n,m;
int main()
{
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--)
{
arr[j] = max(arr[j],arr[j - v[i]] + w[i]);
}
}
cout << arr[m] << endl;
return 0;
}
上边的那个循环枚举的结构其实是由之前的循环体变化过来的
//这个循环结构其实是从注释中的结构体变幻出来的
for(int i = 1;i <= m;i++)
{
for(int j = m j >= 0;j++)
{
arr[j] = arr[j]; //这个是上边的数组去掉一维变换得到的,去掉后是恒等式,所以可以删除,其实意思就是在我没算之前这 //元素其实就是上一层留下来的,所以不用改
if(j >= v[i])//这个判段条件提上去的原因是,因为我们把前一个语句删了,不符合这个条件其实for中相当于是空语句,所以 //还不如放在for中直接少执行几次
{
arr[j] = max(arr[j],arr[j - v[i]] + w[i]);
}
}
}
然后最后输出的arr[m]
其实就是第n层算出来的m体积的结果。
后记
动态规划道阻且长,同志仍需努力