动态规划(一)

背包问题

  • 01背包问题 每件物品最多使用一次
  • 完全背包问题 每件物品有无限个
  • 多重背包问题 每件物品有指定的数量
  • 分组背包问题 每件物品只能从固定的组里选,且每组有且只能选择一件物品

在这里插入图片描述

2.01背包问题

在这里插入图片描述

  • DP
    • 状态表示f[i][j]—指的是从1~i中选出总体积小于j的元素的组合,并且表示的属性是最大值
      • 集合
      1. 所有的选法
      2. 条件
        • 数目限制(1~i)
        • 体积限制(<=j)
      • 属性 max,min,数量 — 本题中的属性指的是最大价值max
    • 状态计算 集合的划分
  • 集合的划分
  • 将集合不重不漏的划分成更小的部分(类似于组合数的公式拆解)使其能够用递归方式进行统计
  • 本题:01背包问题,考虑到集合f[i][j]的本质:从1~i中选出总体积小于等于j的元素的组合,并且表示的属性是最大值,将其拆分为选中元素i,和没有选中元素i的两个部分
  • 其中没有选中元素i的部分即可表示成集合f[i-1][j](即从1~i-1中选择总体积为j的元素并且价值最大)
  • 选中元素i的部分可以表示为f[i-1][j-vi]+wi即将i元素从选法中删除,那么代表最大值的选法应当仍然是最大值的选法,而最后的答案加上i元素的价值即可
  • 所以最后f[i][j]表示为两个选法的最大值即f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])

优化成一维数组

  • 观察到状态方程f[i][j]=max(f[i-1][j],f[i-1][j-v[i]]+w[i])在每次计算时,只用到i-1层的数据,联想到可以用滚动数组进行优化(滚动数组即一种节约空间的优化方式)
  • 滚动数组优化
    如优化斐波那契数列
#include<iostream>
const int N=100;
int q[N];

int main(){
q[0]=1;

q[1]=1;

for(int i=2;i<N;i++)q[i]=q[i-1]+q[i-2];
    
}

注意到这里的每一项q[i]只依赖于前面两项,所以转化为滚动数组

#include<iostream>
const int N=100;

int q[3];

int main(){
q[1]=1;

q[2]=1;

for(int i=2;i<N;i++){
   q[0]=q[1];
   
   q[1]=q[2];

   q[2]=q[0]+q[1];
}

cout<<q[2]<<endl;    //输出第N项
    
}


  • 同理,由于每一次只用到上一行的数组值,这里可以优化成一位状态方程f[j] = max(f[j], f[j-v[i]] + w[i])
  • 注意,状态方程f[i][j]的值只取决于上一层的f[i-1][j-v[i]]和f[i-1][j],如果运用滚动数组并且顺序更新则会出现冲突,如
  • f[i][j-v[i]]取决于f[i-1][j-v[i]]和f[i-1][j-v[i]-v[i-1]]
    f[i][j]的值取决于f[i-1][j-v[i]]和f[i-1][j]
    如果先将f[i-1][j-v[i]]更新成f[i][j-v[i]],那么f[i][j]将无法更新
二维数组01j-1j
0
1
i-1f[i-1][j-v[i]]f[i-1][j]
if[i][j-v[i]]f[i][j]
DP+二维数组
#include<iostream>

using namespace  std;

const int N=1010;

int v[N],w[N];    //体积,价值

int n,m;          //数量,体积

int f[N][N];      //f[i][j]

int main(){
    
    cin>>n>>m;
    
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i];
    
    for(int i=1;i<=n;i++)            //f[0][1~m]的值默认是0
       for(int j=1;j<=m;j++){        //体积从1开始统计到m
           f[i][j]=f[i-1][j];
           
           if(j>=v[i])f[i][j]=max(f[i][j],f[i-1][j-v[i]]+w[i]);   //如果j体积大于等于v[i],(i元素可以被选择)取两者最大值
       }
       
    cout<<f[n][m];
}

一维数组01j-1j
i-1(实际不存在这一行)f[j-v[i]]f[j]
i(i的状态取决于i-1的状态)f[j-v[i]]f[j]
DP+一维数组
#include<iostream>

using namespace  std;

const int N=1010;

int v[N],w[N];    //体积,价值

int n,m;          //数量,体积

int f[N];

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--){         //倒序计算f[],枚举到体积大于等于v[i]
           
           f[j]=max(f[j],f[j-v[i]]+w[i]);
       }
       
    cout<<f[m];
}

3.完全背包问题

在这里插入图片描述

  • 状态表示与01背包问题相同f[i][j]同样表示从1~i元素中选出的元素总体积为j的组合,且为最大值
  • 不同在于状态方程的计算
  • 同样将其拆解为有没有选择i元素的若干部分,由于这里的i虽然有无限多个,但是由于j的束缚也只可以选择有限次,我们假定最多选取n个
  • 那么集合可以划分为:选择0,1,2,…k,k+1,…,n个i
  • 状态方程可以表示为f[i][j]=max(f[i-1][j-k*v[i]]+w[i]*k)k=0,1,...,n-1,n
  • 优化
  • 由于状态方程要进行k重的枚举,时间复杂度接近109 所以我们对状态方程进行优化
  • 观察状态方程
    f[i,j]=max(f[i-1,j],f[i-1,j-v[i]]+w[i],f[i-1,j-2*vi]+2*wi),...f[i-1,j-k*vi]+wi*k.....)
  • f[i][j-v[i]]将其按照同样计算的方式展开,得:
  • f[i,j-vi]=max(f[i-1,j-vi],f[i-1,j-2*vi]+wi,f[i-1,j-3*vi]+2*wi....f[i-1,j-k*vi]+wi*(k-1)...
  • 对比两个多项式可以发现f[i][j-vi]的展开式是f[i][j]的展开式从第二项开始每一项增加一个wi
    由此,可以将f[i][j]的展开式简化为
  • f[i][j]=max(f[i-1][j],f[i][j-v[i]]+w[i])
DP+二维
#include<iostream>

using namespace std;

const int N=1010;

int f[N][N];

int n,m;

int v[N],w[N];

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=1;j<=m;j++){
           f[i][j]=f[i-1][j];
           
           if(j>=v[i])f[i][j]=max(f[i][j],f[i][j-v[i]]+w[i]);
        }
        
    cout<<f[n][m];
}

  • 优化成一维:由于不再需要取决于上一层的状态f[i-1][j-v[i]]按照顺序计算即可
DP+一维
#include<iostream>

using namespace std;

const int N=1010;

int f[N];

int n,m;

int v[N],w[N];

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=v[i];j<=m;j++)
           f[j]=max(f[j],f[j-v[i]]+w[i]);
        
        
    cout<<f[m];
}

4.多重背包问题

在这里插入图片描述

  • 朴素写法与完全背包问题相同,只不过此时枚举的k为给定的有限值s[i]
  • 状态方程可以表示为f[i][j]=max(f[i-1][j-k*v[i]]+w[i]*k)k=0,1,...,s[i]
  • 此时时间复杂度为O(n*v*s)接近106 所以不超时
#include<iostream>

using namespace std;

const int N=110;

int n,m;

int v[N],w[N],s[N];

int f[N][N];

int main(){
    
    cin>>n>>m;
    
    for(int i=1;i<=n;i++)cin>>v[i]>>w[i]>>s[i];
    
    for(int i=1;i<=n;i++)
       for(int j=1;j<=m;j++)
          for(int k=0;k<=s[i];k++)
          if(j>=k*v[i])f[i][j]=max(f[i][j],f[i-1][j-k*v[i]]+k*w[i]);    //枚举每一个可能的k的数量
          
    cout<<f[n][m];
          
}

5.多重背包问题II

在这里插入图片描述

  • 由于数据范围为 1000 ∗ 2000 ∗ 2000 = 4 ∗ 1 0 9 1000*2000*2000=4*10^9 100020002000=4109 所以采用三重循环会超时
  • 若采用与完全背包问题一样的思路优化,不难看出,难以求出后项的最大值

在这里插入图片描述

  • 观察数量:一共有N*S件物品
  • 用二进制数去打包N*S件物品,将其打包成新的组合,并且每个组合的数量只有一个
  • 例如:若s=1023,则我们不需要枚举从j-v,j-2v....j-sv这1023种情况
  • 由于二进制1 000 000 000,十位的二进制可以表示从0~1023的范围,他的每一位相当于打包成了1,2,4,8...512组合的物品,结果我们就把原来的N*S件物品分成了N *logS件物品,
  • 由于新的N*logS件物品每样只有一个,对这些物品做一遍01背包问题即可
  • 倘若s的范围并不在一个恰好的二进制数内,例如s=200
  • 可以将s打包成1,2,4....64这些新的组合数量的物品,即0 000 000 ~ 1 111 1110~127这个范围,并且用一个c件物品,c=200-127=73来填补这个空缺
  • 此处由于二进制最高位k=7,并且c<2^k+1,所以填补的范围不会有空缺
  • 这样一来时间复杂度被降低为 O ( n ∗ v ∗ l o g s ) = 1000 ∗ 2000 ∗ l o g ( 2000 ) = 2 ∗ 1 0 7 O(n * v *logs) =1000*2000*log(2000)=2*10^7 O(nvlogs)=10002000log(2000)=2107log2000~=11
#include<iostream>

using namespace std;

const int N=25000,M=2010;        //N=n*logs~=2000*11=22000个元素,这里开到25000
int n,m;

int v[N],w[N];

int f[M];                      //一维01背包问题

int main(){
    
    cin>>n>>m;
    
    int cnt=0;                
    
    for(int i=1;i<=n;i++){
        int a,b,s;
        
        cin>>a>>b>>s;                           //输入每一件物品的体积,价值,数量
        
        int k=1;
        
        while(k<s){                           //将s分类打包成新的logs组物品
            cnt++;
            
            v[cnt]=a*k;
            
            w[cnt]=b*k;
            
            s-=k;
            
            k*=2;
        }
        
        if(s>0){                           //打包多余的零头
            cnt++;
            
            v[cnt]=a*s;
            
            w[cnt]=b*s;
        }
    }
    
    for(int i=1;i<=cnt;i++)                   //对于所有的N*logS,做一遍01背包问题
       for(int j=m;j>=v[i];j--)
       f[j]=max(f[j],f[j-v[i]]+w[i]);
    
    cout<<f[m];
}

9.分组背包问题

在这里插入图片描述

  • 状态表示与01背包问题相同f[i][j]表示从1~i元素中选出的元素总体积为j的组合,且为最大值
  • 集合划分:
  • 由于每一组物品最多只能选一个或者不选,所以我们考虑f[i][j]的状态方程为选不选第i组当中的某一个物品
  1. 不选:f[i][j]=f[i-1][j]
  2. f[i][j]=f[i-1][j-v[i,k]]+w[i,k],k=1,2,3,.....s[i]
  • f[i][j]=max(f[i-1][j],f[i-1][j-v[i,k]]+w[i,k])
#include<iostream>

using namespace std;

const int N=110;

int n,m;

int v[N][N],w[N][N],s[N];

int f[N];

int main(){
    
    cin>>n>>m;
    
   //初始化每一组元素
    for(int i=1;i<=n;i++){
        cin>>s[i];
        
        for(int j=0;j<s[i];j++)
        cin>>v[i][j]>>w[i][j];              //输入每一组中每一个元素的体积,价值
    }
    
    for(int i=1;i<=n;i++)
       for(int j=m;j>=0;j--)               //与01背包优化相同,因为需要依赖于上一组的值,所以倒序枚举
          for(int k=0;k<s[i];k++)          //枚举每一组所有可能选的元素
          if(j>=v[i][k])f[j]=max(f[j],f[j-v[i][k]]+w[i][k]);
          
    cout<<f[m];
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值