zoukankan      html  css  js  c++  java
  • 01背包问题--动态规划解法(2)(转载)

    本章主要讲述最简单的背包问题,从如何建立状态方程到如何根据状态方程来实现代码,再到如何优化数据结构,让我们对动态规划的建立与求解认识更加透彻

    题目:
    有N件物品和一个容量为V的背包。放入第i件物品的费用是Ci,得到的价值是Wi。求解将哪些物品装入背包可使价值和最大。
    分析:
    (一)建立状态方程
    这是最基础的背包问题,直接说状态转移方程了,设dp[i][v]表示前i件物品放入容量为v的背包能获得的最大价值,每件物品可以选择放与不放,则有:
    dp[i][v]=max{dp[i-1][v],dp[i-1][v-Ci]+W[i]}
    稍微解释一下,当第i件物品不放时,则dp[i][v]=前i-1件物品恰好放入v容量的背包,即dp[i-1][v],当i件物品放时,那就说明前i-1件物品放的容量为v-Ci,这样才能正好将第i件物品放进去,即dp[i-1][v-Ci]+Wi
    (二)根据状态方程实现代码
    状态方程建好之后,下面就是如何将它用代码实现,要考虑的主要问题就是:在给dp[i][v]赋值前,dp[i-1][v]和dp[i-1][v-Ci]必须已经赋过值了,带着这个问题我们来看,对于这样一个转移方程,二维的,肯定需要至少两层遍历,需要考虑两个方面:
    1、这两个二维变量是从前向后遍历还是从后向前遍历
    2、这两个变量的先后遍历顺序
     
    先看第一个方面:
    看到方程里有两个变量,i与v,先看i,i的关系是靠i-1推过来的,所以i毫无疑问一定是从0开始到n遍历,再看v,v一定是比v-Ci大的,所以v也一定是从最小的值0开始到最大的值V,
    然后再来看顺序,即是先遍历i还是先遍历v的问题,我们看到这个状态转移方程的i只与i-1有关,即i变量只变动了1,而此时v变量是从v-Ci到的v,中间变动了Ci,所以此问题毫无疑问,i在外层循环,v在里层循环
     
    最后我们在做下稍微的调整,我们要确保i-1和v-Ci是合法的,所以v不能从0开始了,要从Ci开始,至于i-1,i可以从1开始遍历,用下标1来表示第一件物品,就无需对i-1处理了
     
    根据上面分析,代码就能写出来了,实现代码如下:
     
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #include <iostream>  
    2. #include <algorithm>  
    3. using namespace std;  
    4. #define N 3//物品个数  
    5. #define V 10//背包容量  
    6.   
    7. int dp[N+1][V+1];  
    8. int C[N+1]={0,10,5,5};  //Ci表示第i件物品的费用   (i从1开始)  
    9. int W[N+1]={0,2,2,1};   //W[i]表示第i件物品的价值  
    10.   
    11. int ZeroOnePack(){  
    12.     for(int i=1;i<=N;i++){  
    13.         for(int v=C[i];v<=V;v++)  
    14.         dp[i][v]=max(dp[i-1][v],dp[i-1][v-C[i]]+W[i]);  
    15.     }  
    16.     return dp[N][V];  
    17. }  
    18. int main()  
    19. {  
    20.     memset(dp,0,sizeof(dp));//先对dp初始化  
    21.     cout<<ZeroOnePack();  
    22.     return 0;  
    23. }  

     
    (三)数据结构优化
    接下来,我们要考虑如何对此算法进行优化,
     
    我们先对空间复杂度进行优化
    考虑到i只与i-1有关,所以我们可以用滚动数组的方法,将二维压缩到一维,那么我们要做的就是把i这一维去掉,即当前的i对应的dp[v]是由上一状态i-1对应的dp[v]和dp[v-Ci]得到,那么我们在对当前状态的dp[v]进行赋值时,必须得确保这个dp[v]在内层for循环执行完之前是不能再用到的,如果内层for循环是从Ci开始到V,那么对于dp[v]=max(dp[v],dp[v-Ci]),我们看到max里面的v一定是上一状态i-1对应的dp[v],因为这个dp[v]我们还没用到,(当然了,此时我们已经赋值过的v应是从Ci到v-1),但是对于v-C[i]就不行了,因为v-C[i]很可能就包含在Ci到v-1中,这些值已经变动过了,不再是i-1状态所对应的值了,所以为了保证v-Ci的状态还是之前的状态,我们必须得从后向前遍历,为什么?还是回到方程dp[v]=max(dp[v],dp[v-Ci]),如果是从后向前遍历,那么我们已经赋过值的dp[v]是(dp[V],dp[V-1]......dp[v+1]),那么dp[v]是没动过的,可以,对于dp[v-Ci],v-Ci<v,一定不在(v+1,V)中,所以成立
    做了以上分析,就可以实现代码了
     
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #include <iostream>  
    2. #include <algorithm>  
    3. using namespace std;  
    4. #define N 3//物品个数  
    5. #define V 10//背包容量  
    6.   
    7. int C[N+1]={0,10,5,5};  //Ci表示第i件物品的费用   (i从1开始)  
    8. int W[N+1]={0,2,2,1};   //W[i]表示第i件物品的价值  
    9. int dp[V+1];  
    10.   
    11. int ZeroOnePack(){  
    12.     for(int i=1;i<=N;i++){  
    13.         for(int v=V;v>=C[i];v--)  
    14.         dp[v]=max(dp[v],dp[v-C[i]]+W[i]);  
    15.     }  
    16.     return dp[V];  
    17. }  
    18. int main()  
    19. {  
    20.     memset(dp,0,sizeof(dp));//先对dp初始化  
    21.     cout<<ZeroOnePack();  
    22.     return 0;  
    23. }  

     
    然后我们看时间复杂度,在时间复杂度方面,因为必定要访问到所有的i和所有的v,所以n^2复杂度在所难免,那么我们可不可以以减少些循环的次数?答案是肯定的
    由于我们只需要求最终结果dp[V],对于dp[V-1]、dp[V-2]……我们是不需要知道的,但是上面的代码执行完,经过我们以上的分析,这些值是存在的,所以我们可以感觉到,是不是做了些额外的劳动呢?我们来从最终的结果dp[V]往前找找线索,还是这个公式,我们从最后的V向前看:
    已知最后一步一定做的是这一步:dp[V]=max(dp[V],dp[V-C[N]]+W[N]),我们看出了dp[V]只与dp[V-C[N]]有关,而比容量V-C[N]小的值是不需要计算的,这也是为什么能求出dp[V-1]、dp[V-2]……d的原因,依次在往前推,在i=N-1时,此时的dp[V]=max(dp[V],dp[V-C[N-1]]+W[N-1]),c此时的V也只与V-C[N-1]有关,由于此前v需要遍历的最小值是V-C[N],这里在此基础之上又需知道V-C[N-1]的值,所以v需要的遍历的最小值也只要是V-(C[N]+C[N-1])就行了,…………规律看出来了吧,话不多说,贴代码:
     
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1.   
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #include <iostream>  
    2. #include <algorithm>  
    3. using namespace std;  
    4. #define N 3//物品个数  
    5. #define V 10//背包容量  
    6. int C[N+1]={0,10,5,5}; //Ci表示第i件物品的费用 (i从1开始)  
    7. int W[N+1]={0,2,2,1}; //W[i]表示第i件物品的价值  
    8. int dp[V+1];  
    9. int sum[N+1]={0};//辅助数组,用于求C[N]+C[N-1]……  
    10. int ZeroOnePack(){  
    11.     //常数优化  
    12.     sum[N]=C[N];  
    13.     for(int i=N-1;i>=1;i--)sum[i]+=sum[i+1]+C[i];  
    14.     //end  
    15.     for(int i=1;i<=N;i++){  
    16.     for(int v=V;v>=max(V-sum[i],C[i]);v--)  
    17.     dp[v]=max(dp[v],dp[v-C[i]]+W[i]);  
    18.     }  
    19.     return dp[V];  
    20. }  
    21. int main()  
    22. {  
    23.     memset(dp,0,sizeof(dp));//先对dp初始化  
    24.     cout<<ZeroOnePack();  
    25.     return 0;  
    26. }  
    (四)状态方程深入剖析
    适才我们从最终的dp[V]向前讨论,怎么那么像递推的思想呢,由于只需求dp[V]的解,那么递归能否解决呢
    还是从这个方程开始dp[v]=max(dp[v],dp[v-C[i]]+W[i]),max中前者指的是不取i获得的值,后者指的是取i获得的值,由此可以写递归代码了
     
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #include <iostream>  
    2. #include <algorithm>  
    3. using namespace std;  
    4.   
    5. #define N 3//物品个数  
    6. #define V 10//背包容量  
    7.   
    8. int C[N+1]={0,10,5,5};  //Ci表示第i件物品的费用   (i从1开始)  
    9. int W[N+1]={0,2,2,1};   //W[i]表示第i件物品的价值  
    10.   
    11. int ZeroOnePack(int i,int v){  
    12.     if(i<=0||v<=0)return 0;  
    13.       
    14.     //不选第i件物品  
    15.     int a=ZeroOnePack(i-1,v);  
    16.     //必须得保证剩下的容量v能够有C[i]的容量,才能选择第i件物品  
    17.     int b=v>=C[i]?ZeroOnePack(i-1,v-C[i])+W[i]:0;  
    18.     return max(a,b);  
    19. }  
    20. int main()  
    21. {  
    22.     cout<<ZeroOnePack(N,V);  
    23.     return 0;  
    24. }  

     
    (五)变形问题
    将题目略微改动一下,附加一个条件,即求恰好装满容量为v的背包所获得的最大值,那么我们该如何求解呢?
    我们还是从状态方程开始,从最初的方程说吧,dp[i][v]=max(dp[i-1][v],dp[i-1][v-C[i]]+W[i]),这里的v就要指恰装满v的背包了,我们来看从上一状态i-1怎么转移到当前状态的,dp[i-1][v]和dp[i-1][v-C[i]]两个值,因为这里要求装满背包,我们无法保证dp[i-1][v]和dp[i-1][v-C[i]]是存在的,即前i-1件物品不一定装满容量v的背包或者是容量v-C[i]的背包,但是如果其中一个是能装满的,另一个不能装满,则一定是选那个能装满的,再看是取这两个值得max值,那么我们就有法了,将初态除dp[0][0]=0外,dp[0][1……V]=负无穷,即1……V的背包起始状态是无效态
     
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #include <iostream>  
    2. #include <algorithm>  
    3. #include <vector>  
    4. using namespace std;  
    5. #define INF -0x7ffffff  
    6. #define N 3//物品个数  
    7. #define V 10//背包容量  
    8.   
    9. int C[N+1]={0,10,4,5};  //Ci表示第i件物品的费用   (i从1开始)  
    10. int W[N+1]={0,2,2,1};   //W[i]表示第i件物品的价值  
    11. vector<int> dp;  
    12.   
    13. int ZeroOnePack(){  
    14.     for(int i=1;i<=N;i++){  
    15.         for(int v=V;v>=C[i];v--)  
    16.         dp[v]=max(dp[v],dp[v-C[i]]+W[i]);  
    17.     }  
    18.     return dp[V];  
    19. }  
    20. int main()  
    21. {  
    22.     dp.assign(V+1,INF);//dp初始化为负无穷  
    23.     dp[0]=0;  
    24.     cout<<ZeroOnePack();  
    25.     return 0;  
    26. }  

     
    递归代码实现也只要稍微改一改条件就行了
     
    [cpp] view plaincopy在CODE上查看代码片派生到我的代码片
     
    1. #include <iostream>    
    2. #include <algorithm>    
    3. using namespace std;    
    4.     
    5. #define INF -0x7fffffff  
    6. #define N 3//物品个数    
    7. #define V 10//背包容量    
    8.     
    9. int C[N+1]={0,10,4,5};  //Ci表示第i件物品的费用   (i从1开始)    
    10. int W[N+1]={0,2,2,1};   //W[i]表示第i件物品的价值    
    11.     
    12. int ZeroOnePack(int i,int v){    
    13.     if(v==0)return 0;  
    14.   
    15.     if(i==0)return INF;  
    16.   
    17.     //不选第i件物品    
    18.     int a=ZeroOnePack(i-1,v);    
    19.     //必须得保证剩下的容量v能够有C[i]的容量,才能选择第i件物品    
    20.     int b=v>=C[i]?ZeroOnePack(i-1,v-C[i])+W[i]:INF;    
    21.     return max(a,b);    
    22. }    
    23. int main()    
    24. {    
    25.     cout<<ZeroOnePack(N,V);    
    26.     return 0;    
    27. }   

     
     
  • 相关阅读:
    DSP、SSP、RTB、ADX(概念通俗解释)
    面试被问到你的优点和缺点时,该如何完美的回答
    android中文api(79)——Gallery
    RelativeLayout用代码兑现布局
    关于LayoutInflater的错误用法
    android 自定义照相机Camera黑屏 (转至 http://blog.csdn.net/chuchu521/article/details/8089058)
    Android camera 竖直拍照 获取竖直方向照片
    java动态代码的实现以及Class的卸载 (转至http://dustin.iteye.com/blog/46393)
    Java类变量和成员变量初始化过程
    下载最新android adt的方法
  • 原文地址:https://www.cnblogs.com/bendantuohai/p/4622583.html
Copyright © 2011-2022 走看看