球球速刷LC之DP--背包问题

背包DP

背包问题分为01背包与完全背包
01背包,共有N个物体,每个物体只有一个,装入给定背包中
完全背包,共有N种物体,每个物体数量不限,装入给定背包中
01背包
重点:
1.思路:对每个物体i,在剩余容量j时选择装与不装
2.注意一维情况时的容量倒序遍历

问题定义:给定容量V的背包,和体积分别为{Ci}(i=1...N)的N个物体,每个物体价值为{Wi}(i=1..N).求使得背包价值最大的装法。
状态:对于每个物体i,在背包容量为j时,背包价值为DP(i,j)
选择:
     如果当前背包体积j>=Ci,有装与不装当前物体两种选择
         选择装入当前物体,DP(i,j)=DP(i-1,j-Ci)+Wi (j>=Ci)
         不装入当前物体,   DP(i,j)=DP(i-1,j)
    如果剩余背包体积

故转移方程:

for i=1....N
   for j=0...V
     if(j>=Ci) DP(i,j)=max{DP(i-1,j),DP(i-1,j-Ci)+Wi};
     if(j

优化为1维数组
由原始递推式,DP(i,j)仅仅依赖于DP(i-1,XX)状态,考虑优化为1维数组。由于计算DP(i,j)时需要保证DP(i-1,j-Ci)还没有更新,因此容量需要倒序遍历

for i=1....N
   for j=V...0 //此处注意:由于在计算DP(j)时需要DP(j-Ci)依然保持在i-1的状态,所以V需要倒序遍历
      if(j>=Ci)DP(j)=max{DP(j),DP(j-Ci)}

01背包其他问法:根据问法更改转移方程的函数
<1>是否能恰好装满
二维状态定义:DP(i,j)代表第i个物体容量为j时是否恰好装满

二维状态转移方程:
DP(i,j)=DP(i-1,j)||DP(i-1,j-Ci) (j>=Ci)
DP(i,j)=DP(i-1,j)               (j=Ci)(j=V....0) //注意01背包问题1维状态需要倒序遍历
初始化
DP[0]=TRUE    //容量为0时一个物体都不选恰好能装满,故DP[0]=1;
DP[1....V]=FALSE

<2>恰好装满的方案总数
二维状态定义:DP(i,j)代表第i个物体容量为j时恰好装满的总方案数

二维状态转移方程:
DP(i,j)=DP(i-1,j)+DP(i-1,j-Ci) (j>=Ci)
DP(i,j)=DP(i-1,j)              (j=Ci)(j=V....0) //注意01背包问题1维状态需要倒序遍历
初始化
DP[0]=1 //容量为0时至少存在 一个物体都不选这个方案,故初始化为1
DP[1....V]=0

<3>恰好装满所需最少物体数
二维状态定义:DP(i,j)代表第i个物体容量为j时恰好装满所需最少物体数,物体数方案不存在返回-1

二维状态转移方程:
DP(i,j)=min{DP(i-1,j),DP(i-1,j-Ci)+1}(j>=Ci)
DP(i,j)=DP(i-1,j) (j

完全背包

问题定义:给定n种物体,每种物体数量不限,每种物体的体积为Ci,价值为Wi。
向容量为V的背包中装入这n种物体,求背包能装入的最大价值。与01背包的最大不同是,此时每种物体的数量不限。
状态定义:在对第i种物体进行选择,背包体积为j时。DP(i,j)表示此时背包最大价值
选择:
当前物体体积小于背包容积j,则可选择装入或不装入
装入:DP(i,j)=DP(i,j-Ci)+Wi  //此处与01背包最大不同是,由于选择装入物体i后,可以选择继续装入物体i。故此处为DP(i,j-Ci)而不是DP(i-1,j-Ci)
不装入:DP(i,j)=DP(i-1,j)
当物体i体积大于j,只能选择不装入
DP(i,j)=DP(i-1,j)

状态转移方程:

DP(i,j)=max{DP(i-1,j),DP(i,j-Ci)+Wi} (j>=Ci)
DP(i,j)=DP(i-1,j) (j

完全背包一维优化
由状态转移方程,DP(i,j)最多依赖于DP(i-1,j),且计算DP(i,j)时需要DP(i,j-Ci)已经计算。
故一维状态方程

for i=1.....N
   for j=Ci....V //为保证j>=Ci ,j从Ci开始遍历
      DP(j)=max{DP(j),DP(j-Ci)+Wi}   //注意此时对容量遍历是正序遍历,与01背包正好相反

其他问法:
完全背包也包括恰好装满方案数、装满所需最少数量、能否恰好装满等问法。

leetcode 练习

416 分割等和子集(0-1背包,是否恰好能装下)

    bool canPartition(vector<int>& nums) {
        if(nums.empty()){
            return false;
        }        
        unsigned int sum=0;
        for(auto i:nums) sum+=i;
          
        if(sum%2!=0) return false;
        //问题转化为在nums中是否存在n个数之和恰好为sum/2
        //转化为01背包问题,求是否能恰好装满
        unsigned int target=sum/2;
        vector<vector<bool>>dp(nums.size()+1,vector<bool>(target+1,false));
        dp[0][0]=true; //dp[0][0]代表一个物体都不选,恰好能装满容量为0背包
        //转移方程
        //DP(i,j)=DP(i-1,j)||DP(i-1,j-Ci) j>=Ci
        //DP(i,j)=DP(i-1,j) j
        for(int i=1;i<=nums.size();++i){
            for(int j=0;j<=target;++j){
                //当前选择第i个物体,体积为nums[i-1],背包容量为j.
                int Ci=nums[i-1];
                if(j>=Ci){
                    dp[i][j]=dp[i-1][j]||dp[i-1][j-Ci]];
                }else{
                    dp[i][j]=dp[i-1][j];
                }
            }
        }
        return dp[nums.size()][target];
    }

优化为1维状态

    bool canPartition(vector<int>& nums) {
        if(nums.empty()) return false;
        int sum=0;
        for(auto i:nums)sum+=i;
        
        if(sum%2==1) return false;
        int pack=sum/2;//背包容量为和的一半
        
        //01背包问题:状态方程
        //F(i,v)=F(i-1,v)||F(i-1,v-Ci) (v>Ci)
        //F(i,v)=F(i-1,v-Ci) (V
        //一维优化:F(v)=F(v)||F(v-ci) (v=V....Ci) //此时V要倒着遍历,才能保证计算F(i,v)时,F(i-1,v-ci)还没有被更新
        vector<bool>dp(pack+1,false);
        dp[0]=true;//容量0 即一个都不选方案,可行
        for(int i=0;i<nums.size();++i){
            for(int j=pack;j>=nums[i];--j){
                dp[j]=dp[j]||dp[j-nums[i]];
            }
        }
        return dp[pack];
    }

494 目标和 (01背包-求方案数)

   int findTargetSumWays(vector<int>& nums, int S) {
        //题目转换:
        //假设Sp代表取+号数字 Sn 代表取负号数字,sum=sum(nums)则有:
        //Sp+Sn=sum (1)
        //Sp-Sn=S         (2)
        //联合(1)(2)得:Sp=(sum+S)/2
        //即转化为背包容量为(sum+S)/2 的01背包问题
     
        //step1:求和sum
        int sum=0;
        for(auto i:nums) sum+=i;
        //step2:一个直观优化,避免很大的S超时。
        if(S>sum || S<(-sum)) return 0;
        //step3:如果不能平分,只接返回
        if((S+sum)%2==1) return 0;
     
        //step4:求解01背包问题
        int pack=(S+sum)/2;
          //背包容量 pack
         //dp[i][j]代表第i个物体的背包容量为j
         //F(i,v)=F(i-1,v)+F(i-1,v-ci) v>=Ci
         //F(i,v)=F(i-1,v) v
         //=>F(v)=F(v)+F(v-Ci) v=V...Ci
        vector<int>dp(pack+1);
        dp[0]=1;//容量为0时至少存在都不选这个方案
        for(int i=0;i<nums.size();++i){
            for(int j=pack;j>=nums[i];--j){
                //当前选择第i个数字,背包容量为j.
                //背包容量恰好等于当前物体数字
                    dp[j]=dp[j]+dp[j-nums[i]];               
            }
        }
        return dp[pack];        
    }

322 零钱兑换 (完全背包,恰好装满最少数量)

//F(i,v)表示选取第i种物体,背包容积为v时所需最少物体数目
//完全背包 :F(i,v)=min{F(i-1,v),F(i,v-Ci)+1}
//==> F(v)=min{F(v),F(v-Ci)+1}
//进一步优化初始化值,从而避免内部对无效状态的判断
class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if(coins.empty()) return -1;
        if(amount==0) return 0;
        //-1代表无效方案
        //dp[i] 代表容量为i的最小个数
        vector<int>dp(amount+1,amount+1);//  !!!此处小技巧:将无效值初始化为不可能的最大值
        dp[0]=0;//初始条件 amount为0时最小个数为0;        
        for(int i=0;i<coins.size();++i){
            for(int j=coins[i];j<=amount;++j){//另外一个小技巧:将j初始值设为当前物体,避免对是否>=Ci的判断
                   dp[j]=min(dp[j],dp[j-coins[i]]+1);
            }
        }
        return dp[amount]>amount?-1:dp[amount];  //将无效状态转换为题目的-1      
    }
};

二维解法

class Solution {
public:
    int coinChange(vector<int>& coins, int amount) {
        if(amount==0) return 0;
        if(coins.empty()) return -1;
        
        vector<vector<int>>dp(coins.size()+1,vector<int>(amount+1,-1));
        dp[0][0]=0;
        
        for(int i=1;i<=coins.size();++i){
            for(int j=0;j<=amount;++j){
                if(j==0){ //总数为0所需物体数目为0
                    dp[i][j]=0;
                    continue;
                }
                //不选当前物体
                dp[i][j]=dp[i-1][j];
                //选当前物体
                if(j>=coins[i-1] && dp[i][j-coins[i-1]]>=0){
                    //注意完全背包问题,即每一种物体数量不限,此处为dp[i][j-coins[i-1]] 而不是dp[i-1][j-coins[i-1]]
                   dp[i][j]= dp[i][j]>0? min(dp[i][j],dp[i][j-coins[i-1]]+1):dp[i][j-coins[i-1]]+1;
                }
            }
        }
        return dp[coins.size()][amount];        
    }
};

518 零钱兑换 II (完全背包-求方案数)

//F(i,j)=F(i-1,j)+F(i,j-Ci) j>=Ci
//F(i,j)=F(i-1,j) j
//==>F(j)=F(j)+F(j-Ci) j=Ci....V
class Solution {
public:
    int change(int amount, vector<int>& coins) {
        if(amount==0) return 1;
        if(coins.size()==0) return 0;
        vector<int>dp(amount+1,0);
        dp[0]=1;//容量为0时至少存在一个都不选这个方案,故初始化为1
        for(int i=0;i<coins.size();++i){
            for(int j=coins[i];j<=amount;++j){
                dp[j]=dp[j]+dp[j-coins[i]];
            }
        }
        return dp[amount];        
    }
};

二维解法

class Solution2 {
public:
    int change(int amount, vector<int>& coins) {
        if(amount==0) return 1;
        if(coins.size()==0) return 0;
        
        sort(coins.begin(),coins.end());
        //dp[i][j] 表示对前i个物体,容量为j时的方案数目
        vector<vector<int>>dp(coins.size()+1,vector<int>(amount+1,0));
        dp[0][0]=1;
        for(int i=1;i<=coins.size();++i){
            for(int j=0;j<=amount;++j){
                //不选第i个物体
                 dp[i][j]=dp[i-1][j];
                //可以选第i个物体
                if(j>=coins[i-1]){
                    dp[i][j]+= dp[i][j-coins[i-1]]; //此处由于数量无限,故是dp[i][j-coins[i-1]]
                }
            }
        }
        return dp[coins.size()][amount];
        
    }
};

279 perfect squares(恰好装满最少物体数量)

//状态转移方程:
//完全背包问题:
//dp(i,v)=min{dp(i-1,v),dp(i,v-ci)+1};
//转换为1维数组
//dp(v)=min{dp(v),dp(v-ci)+1}; v=ci....V
class Solution {    
public:
    int numSquares(int n) {
        //找到等待装入背包物体种类,即完全平方数;
        vector<int>objects;
        for(int i=1;i*i<=n;++i){
            objects.push_back(i*i);
        }
        
        vector<int>dp(n+1);
        for(int i=0;i<dp.size();++i){
            dp[i]=i;//初始化为最大可能
        }
        
        for(int i=0;i<objects.size();++i){  
            for(int j=objects[i];j<=n;++j){
                dp[j]=min(dp[j],dp[j-objects[i]]+1);
            }
        }
        return dp[n];
    }
};

343 Interger break

class Solution {
public:
    int integerBreak(int n) {
        if(n<=1) return 0;
        //转换为完全背包问题
        //物体为1....n-1 每种数量不限 背包容量n (最大物体为n-1而不是n可以保证至少两个物体才能装满背包)
        //背包价值为背包内物体的乘积,当包内只有一个物体,背包价值就是物体i
        //F(i,j)=max{F(i-1,j),Ci} //j==ci
        //F(i,j)=max{F(i-1,j),F(i,j-Ci)*Ci } j>Ci
        //==>F(j)=max{F(j),F(j-Ci)*Ci}
        
        vector<int>dp(n+1,0);
        dp[0]=0;
        dp[1]=1;
        
        for(int i=1;i<=n-1;++i){
            for(int v=i;v<=n;++v){
                if(v==i){
                    dp[v]=max(dp[v],i);
                }else{
                    dp[v]=max(dp[v],dp[v-i]*i);
                }
            }
        }
        return dp[n];        
    }
};

二维背包问题
474 ones and zeros

class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        //二维费用背包,每个物体(string) 有两种体积,0的数目与1的数目。背包的两种容量为m n
        //01二维费用背包
        //F(i,va,vb)=max{F(i-1,va,vb),F(i-1,va-Cia,vb-cib)}
        //==> F(va,vb)=max{F(va,vb),F(va-Cia,vb-Cib)}  Va=m....cia,Vb=n...Cib
        
        vector<vector<int>>dp(m+1,vector<int>(n+1,0));
        for(int i=0;i<strs.size();++i){
            //求当前物体的两种体积
            int Cia=0;
            for(auto c:strs[i]){
                if(c=='0')++Cia;
            }
            int Cib=strs[i].size()-Cia;
            
            for(int va=m;va>=Cia;--va){
                for(int vb=n;vb>=Cib;--vb){
                    dp[va][vb]=max(dp[va][vb],dp[va-Cia][vb-Cib]+1);
                }
            }           
        }
        return dp[m][n];
    }
};

参考文章:
0-1背包
完全背包
背包九讲
背包细节解析

你可能感兴趣的:(leetcode)