一.动态规划与递推解决01背包

初步分析:

0. 浅谈问题的分解

在处理到第i个物品时,可以假设一共只有i个物品,如果前面i-1个物品的总的最大价值已经定下来了,那么第i个物品选不选将决定这1~i个物品能带来的总的最大价值

刚刚是自顶向下,接下来反过来自底向上,第1个物品选不选可以轻松地用初始化解决,接下来处理第i个物品时,假设只有2个物品就好,那他处理完后前2个物品能带来的最大总价值就确定了,这样一直推下去,就可以推出前n个物品处理完后能带来的最大总价值

 

1.分层考虑解决"每个物品最多只能装一次"

每个物品只能装一次,那么就应该想到常用的一种方法,就是用数组的纵轴来解决,对于n个物品,为它赋予i=1~n的编号,那么数组的纵轴就有n层,每层只考虑装不装这个物品,那么分层考虑就可以解决最多装一个的问题了

 

2.对0,1的理解

对于每个背包,都只有0和1的情况,也就是拿或者不拿两种情况

如果拿:那么空间就会减一点,比如说现在在考虑第i个物品拿不拿,如果说当前剩余空间为j,那么拿了之后空间就变为j-c[i],但是总价值却会增加一点,也就是增加w[i]

如果不拿:那么空间不会变,还是j,但是总价值也不会变化

 

3.限制条件

所以对于这题来说有一个限制条件,就是空间不超出,然后目标就是在空间不超出的情况塞入物品使总价值最大,在前面,我们已经讲了数组的纵轴用来表示当前处理到第几个物品,那么只靠这个是不够的,而且这个数组的意义还没有讲

这题就是限制条件(空间)与价值的平衡,你往背包中塞东西,价值多了,可是空间少了,这空间本来可能遇到性价比更高的物品但也可能没遇到

4.具体的建立数组解决问题

有了前面的限制情况和0,1的分析就可以建立数组了

对于这个数组,结合题目要求来说,数组的意义肯定是当前的总价值,也就是第i个物品的总价值,那么题目还有一个限制条件,只靠一个n层的一维数组是不够的,还需要二维数组的横轴来分析当前的剩余容量

所以我们有了一个数组可以来解决问题了,这个数组就叫f好了,然后它是一个二维数组,它的纵轴有i层,我希望它从i=1~n,不想从下标0开始是为了美观,然后这个二维数组的横轴代表着当前剩余的空间,就用j来表示,j=0~V,0就是没有空间的意思,V前面说了,是这个背包的总容量

为什么要遍历v~0?

其实这个就是为了遍历放这个 i 物品的所以空间情况

我们把这个二维数组建立在int main()的上面,所以它一开始全部都是0,省去了接下来赋初值为0的功夫

有了数组f[i][j],然后对于每个f[i][j],它表示的是已经处理到第i个物品了,当剩余空间还有j时,能带有的最大价值,也就是说f[i][j]存储的是总价值

说是总价值,可是涉及到放物品还是不放物品的问题,所以再细致点就是:当前剩余空间为j,用这j空间取分析第i个物品装不装如,处理执行完行为后,f[i][j]就表示了当前能装入的最大价值

 

5.推导递推方程

一个物品,有放与不放两种选择,我们的最终结果就是综合所有物品放与不放的选择来确定。
有这样一个状态方程:
F[ i ][ j ]=max( F[ i-1 ][ j ] ,F[ i-1 ][ j - wi[i] ] + vi[ i ] );
 
其实不难理解,F[ i ][ j ]  代表 当放到第 i 个物品时,此时容量还剩 j ,我们可以选择放或者不放;
不放的话 F[ i ][ j ]=F[ i-1 ][ j ];就是相当于放了前 i-1 个物品中的物品;注意,不一定是前 i-1 个
物品全都放进了背包。第 i 个物品放进去的话 F[ i ][ j ]=F[ i-1 ][ j - wi[i] ] + vi[ i ]; 放进去了所以
在前面放了 i-1 个物品中的物品的基础上,容量减去 wi[i], 价值加上vi[ i ](但得在wi[i] <= j 条件下)

为什么要用max函数? 

有一些偏极端情况,放入这个物品,也许它价值w[i]很高,但是它占用空间c[i]也大,它的性价比可能很低,所以这时候就需要max函数了

当还有空间时:F[i,j] = max[F[i−1,j],F[i−1,j−C[i]] + W[i]

当空间不够时:F[i,j] = F[i−1,j]

下面这个就是01背包的普通代码:

 

 1 #include<bits/stdc++.h>//万能头文件
 2 #define ll long long
 3 using namespace std;
 4 const ll maxn=100;
 5 ll n,v,f[maxn][maxn];
 6 ll c[maxn];//每个物品占用空间
 7 ll w[maxn];//每个物品的价值
 8 int main()
 9 {
10     cin>>n>>v;
11     for(ll i=1;i<=n;i++)
12         scanf("%lld",&c[i]);
13     for(ll i=1;i<=n;i++)
14         scanf("%lld",&w[i]);
15     for(ll i=1;i<=n;i++)//第i个物品
16         for(ll j=v;j>=0;j--)//剩余空间j
17         {
18             if(j >= c[i])//如果装得下
19                     f[i][j]=max( f[i-1][j-c[i]]+w[i],f[i-1][j]);
20             else//如果装不下
21                 f[i][j]=f[i-1][j];
22         }
23     cout<<f[n][v]<<endl;//输出答案
24 
25 }

 

 

 

二.01背包的空间优化

有了前面基础版01背包的学习,现在学习这个就容易多了

1.何为空间优化,为什么要空间优化

在01背包中通过对数组的优化(用了滚动数组的方法),可以使本来N*V的空间复杂度降低到V,也就是把关于第几个物品的N去掉了(下面会解释为什么可以这么做)

至于为什么要空间优化,首先是因为递推本来就是用空间换时间,消耗的空间比较大,然后关于算法的竞赛一般都会有空间的限制要求,最后,在找工作面试时,面试官肯定会问一些优化的问题,平时养成优化的习惯面试时也有好处

2.为什么这题可以降维

通过观察可以发现对于普通版的01背包递推式,f[i][...]只和f[i-1][...]有关,那么我们可以用一种占用,一种滚动的方法来循环使用数组的空间,所以这个方法叫滚动数组,对于将来肯定用不到的数据,直接滚动覆盖即可,具体的如何滚动会放下面讲

还有就是滚动数组的缺点是牺牲了抹除了大量数据,不是每道题都可以用,但是在这,答案刚好是递推的最后一步,所以直接输出即可,递推完后不需要调用那些已经没了的数据,所以这题可以

下面先画个图理解一下滚动的大致概念

反正就是不断覆盖的过程

3.这题如何具体优化

下面开始具体化的分析

对于第i层,它只和第i-1层有关,但是对于剩余空间j无法优化,所以现在拿i开刀,把他砍掉,用一个长度为V(总空间)的数组来表示,然后每次相邻的两个i和i-1在上面一直滚动

所以现在建立一个数组f[V],一维数组大小为V

首先建立两个复合for循环

for(i=1~n)

  for(j=v~0)

记住这里第二层循环必须是v~0而不是0~v,先记着,后面会解释,

接下来的分析建议配合下面图片学习

然后在循环的过程中,还是老样子,假设我们已经循环到i=2这层了(也就是说i=1已经循环完了),然后对于i=2这一层,我们对j循环,j从v到0

假如现在j=v,我们让f[j]=max(f[j],f[j-c[i]]+w[i])

在没有覆盖之前,所有的f数据都是属于上一层也就是第一层的,我们就当作i-1层数据已经准备好了,然后把max内的拆成两半分析,对于f[j]=f[j]就是不放的情况,那么总价值没有改变,所以对于f[j]=f[j]就是形式上的更新数据,把i-1层的f[j],给了i-1层的f[j]...对于f[j]=f[j-c[i]]+w[i],那个w[i]是肯定要加的不用讨论,然后我们观察一下,对于下标j-c[i]是不是肯定会小于j,那么如果说j从V~0也就是从最大到最小,每次赋值处理都是从前面的格子看看数据参考,并没有修改

再详细点说的话就是对于f[j]=f[j-c[i]]+w[i],f[j-c[i]]是第i层的东西,让j=v~0是为了让f数组每次滚动覆盖时都是覆盖接下来不需要用的位置,比如说处理到第f[8]位时,假如接下来的max判定后面那种方法总价值大,然后假设c[i]=3,这时后就相当与f[8]=f[8-c[i]=5]+w[i],我们这里只是参考了f[5]的数据,并没有改变它,因为说不定计算新一轮f[6]时又要用到旧的f[5]呢,可是我们刷新了f[8]的数字后,再j--,计算f[7],再j--,计算f[6],都不会再用到f[8]这个数据,这是由于f[j-c[i]] 中的减c[i]导致的,反之,假若我们让j=0~v,就可能出现新数据被新数据覆盖的结果,我们是有"底线"的,只允许新数据覆盖旧数据

对于j,如果要处理f[j]=max(f[j],f[j-c[i]]+w[i]),就得当j>=c[i]时处理,因为如果j<c[i],那么j-c[i]为负,下标负的情况没必要考虑,如果考虑了还可能会溢出

 其实对于max,还用另一个小东西代替,有没有发现,如果f[j-c[i]]+w[i]>f[j],就选f[j-c[i]]+w[i],如果f[j-c[i]]+w[i]<f[j],那选f[j]和没选一样,所以待会的空间优化版省掉了max函数,少用一种函数

 

#include<bits/stdc++.h>//万能头文件
#define ll long long
using namespace std;
const ll maxn=100;
ll n,v,f[maxn];
ll c[maxn];//每个物品占用空间
ll w[maxn];//每个物品的价值

int main()
{
    cin>>n>>v;
    for(ll i=1;i<=n;i++)
        scanf("%lld",&c[i]);
    for(ll i=1;i<=n;i++)
        scanf("%lld",&w[i]);
    for(ll i=1;i<=n;i++)//第i个物品
        for(ll j=v;j>=0;j--)//剩余空间j
        {
            if(f[j]<=f[j-c[i]]+w[i] && j-c[i]>=0 )//二维数组变一维数组
                 f[j]=f[j-c[i]]+w[i];//如果值得改变并且j的空间还装得下就赋新值
        }
    cout<<f[v]<<endl;//输出答案

}

 

三.初始化的细节

 

 

原博客:https://www.cnblogs.com/zyacmer/p/9961710.html