0-1背包和完全背包问题、LeetCode416题、1049题、494题、322题

0-1背包问题

假设N件物品,每件物品重量为w[i],每件物品的价值为v[i],背包重量为C,选择物品装入背包使其在不超过负重的前提下价值最大,每种物品只能用一次。

假设dp[i][j]表示在前i件物品,背包重量为j的前提下的最大价值
则如果物品可以放入背包,则动态转移方程为 dp[i][j]=max( dp[i-1][j],dp[i-1][j-w[i]]+v[i] )  , j>=w[i]
如果物品不能放入背包,dp[i][j]=dp[i-1][j]  ,j<w[i]

#include <bits/stdc++.h>

using namespace std;

#define M 1010
int dp[M][M];  //数组太大定义为全局变量,一方面自动初始化,一方面数组范围可以更大

int main()
{
    
    
    int m,N,C,w[M],v[M];
    cin>>m;
    while(m--){
    
    
        cin>>N>>C;
        for(int i=1;i<=N;i++)  cin>>v[i];
        for(int i=1;i<=N;i++)  cin>>w[i]; //这里数组是w和v都是从下标1开始存储数据的
        for(int i=1;i<=N;i++){
    
       //一个二维表,从第二行开始计算,第一行全部初始化为0
            for(int j=0;j<=C;j++)
                if(j>=w[i]) dp[i][j]=max(dp[i-1][j],dp[i-1][j-w[i]]+v[i]);
                else dp[i][j]=dp[i-1][j]; //dp数组默认初始化,默认全是0,所以第一行dp[0][j]也全部是0,
        }//就不用再额外自己初始化了,即for(int j=0;j<=C;j++)  dp[0][j]=0;省略
        cout<<dp[N][C]<<endl;
    }
    return 0;
}

为什么说要把打表出来的数组dp第一行全部初始化为0呢?
因为dp[0][j] 是指没有任何物品的前提下装满容量为j的背包,价值肯定为0,则dp[1][j]代表第一件物品装满容量j的背包的最大价值,注意,看数组w和v中存储数的下标,有些是从0开始存储数据的,此时对应w[i-1],有些是从下标1存储数据的,则直接对应下标w[i],下面的例子都是这样的道理,不再赘述!

下面对空间复杂度进行优化,观察到dp[i][j]的取值取决于上一行dp[i-1][j],所以在只要最终结果的前提下,我们只需要保留两行就行了,第0行和第1行,利用滚动数组,使其不断在0和1之间变换:
利用异或,假设p=0; 让p一直和1异或 ,p=p^1=1;  p=p^1=0;  p=p^1=1,就这样一直变换

#include <bits/stdc++.h>

using namespace std;

#define M 1010
int dp[2][M];

int main()
{
    
    
    int m,N,C,w[M],v[M];
    cin>>m;
    while(m--){
    
    
        cin>>N>>C;
        int p=0;
        memset(dp[p],0,sizeof(dp[p]));//由于是多次输入,得对第0行进行初始化,不能再依靠系统的默认初始化
        for(int i=1;i<=N;i++)  cin>>v[i];
        for(int i=1;i<=N;i++)  cin>>w[i];
        for(int i=1;i<=N;i++){
    
    
            p^=1;   //在第0行和第1行之间来回切换
            for(int j=0;j<=C;j++)
                if(j>=w[i]) dp[p][j]=max(dp[p^1][j],dp[p^1][j-w[i]]+v[i]);
                else dp[p][j]=dp[p^1][j];
        }
        cout<<dp[p][C]<<endl;
    }
    return 0;
}

上面的滚动数组数组属于过渡阶段,可以借鉴这样的思想,采用这样的思路直接使用一维数组。
注意到,算dp[i][j]时只于它上一行在第j列之前的数组有关,所以我们在第二层for循环时采用倒序,从j=C入手,逐渐递减,判定结束的条件是背包容量j是否大于当前要装入背包的物品的重量w[i],因为当小于w[i]时,由前面知是直接dp[i][j]=dp[i-1][j],继承上一行的数据,由于我们采用一维数组,可直接继承。

#include <bits/stdc++.h>

using namespace std;

#define M 1010
int dp[M];

int main()
{
    
    
    int m,N,C,w[M],v[M];
    cin>>m;
    while(m--){
    
    
        memset(dp,0,sizeof(dp));
        cin>>N>>C;
        for(int i=1;i<=N;i++)  cin>>v[i];
        for(int i=1;i<=N;i++)  cin>>w[i];
        for(int i=1;i<=N;i++){
    
    
            for(int j=C;j>=w[i];j--) //倒序
                dp[j]=max(dp[j],dp[j-w[i]]+v[i]);
        }//注意第二层for循环结束的条件是j>=c[i],因为当j<c[i]时,不做处理,默认继承上一行的数据 
        cout<<dp[C]<<endl;
    }
    return 0;
}

LeetCode416. 分割等和子集

这题相当于是背包负重sum/2,物品的重量和价值都是nums[i],价值V给出,希望是sum/2。
分析:
二维数组dp[i][j]表示前i件物品能否使容量为j的背包价值为j,能则为1/true,不能则为0/false。
动态转移方程为dp[i][j]=dp[i-1][j] || dp[i-1][j-nums[i-1]] //i-1是因于代码给出的nums数组是从0开始存储的
方程可以理解为:如果前i-1件物品已经凑够价值j了,那么前i件物品也肯定可以,或者前i-1件物品凑出了j-nums[i-1]的价值,再加上这个物品,就够j了,也为true。
下面简化成一维数组dp

class Solution {
    
    
public:
    bool canPartition(vector<int>& nums) {
    
    
        int sum=0,size=nums.size();
        for(int i=0;i<size;i++)  sum+=nums[i];
        if(sum%2)  return false;   //如果是奇数,肯定不能分割
        sum/=2;
        int dp[sum+1];
        memset(dp,0,sizeof(dp));
        dp[0]=1;   //和前面的一般01背包进行理解,思考为什么这里要为1?
        for(int i=1;i<size+1;i++)
            for(int j=sum;j>=nums[i-1];j--)
                dp[j]=dp[j]||dp[j-nums[i-1]];
        
        return dp[sum];
    }
};

dp[0][j] 表示前0件物品,即空,使背包价值为j,显而易见,当j为0时,dp[0][0]为1。
所以在用一维数组处理时,dp[0]为1,其它初始化为0。

LeetCode1049. 最后一块石头的重量 II

此题换个理解方式即为:将这组石头分成两组,使这相差的重量之差最小,返回这个差值。
求出一个分组的重量就行,假设总重量为ss的石头分为重量为a和重量为b的两组(b>=a),那么差值即为b-a,等价于(b+a)-(a+a),即ss-2*a,且两组分组肯定有一组大于等于ss/2,一组小于等于ss/2。
因此,将问题转换为0-1背包问题,对于重量和价值均为stones[i]的石头,背包负重为ss/2,求最大价值。标准0-1背包模板!

class Solution {
    
    
public:
    int lastStoneWeightII(vector<int>& stones) {
    
    
          int size=stones.size();
          int ss=0;
          for(int i=0;i<size;i++)  ss+=stones[i];
          int sum=ss/2;
          int dp[sum+1];
          memset(dp,0,sizeof(dp));
          for(int i=1;i<size+1;i++)
             for(int j=sum;j>=stones[i-1];j--)
                 dp[j]=max(dp[j],dp[j-stones[i-1]]+stones[i-1]);
        return ss-2*dp[sum];
    }
};

LeetCode494. 目标和

添加正负号,通过移项变号可知,此题和上面的题一样,也是把数组分成两组,假设数组和为sum,分成的两组中A组和为a,B组和为b,如果成立的话,则有a+b=sum,a-b=S,那么可知a=(sum+S)/2,b=(sum-S)/2。
即如果sum<S或者sum+S/sum-S不是2的倍数的话,就不能划分成功,即有0种方法。
动态转移方程:dp[i][j]=dp[i-1][j]+dp[i-1][j-nums[i-1]],即考虑加入nums[i-1]和不加入nums[i-1]两种情况下的方法总数,如果j<nums[i-1],即不能加入,直接取dp[i-1][j]的值。
同样的,dp[0]][0]表示0个数分组相差为0的情况,所以dp[0]][0]=1,其它dp[0][j]=0。
同理采用一维数组表示:

class Solution {
    
    
public:
    int findTargetSumWays(vector<int>& nums, int S) {
    
    
         int n=nums.size();
         long long  sum=0;  //防止sum超出范围
         for(int i=0;i<n;i++)   sum+=nums[i];
         if((sum+S)%2==1 || sum<S)  return 0;
         sum=(sum+S)/2;  //算其中一个就行
         int dp[sum+1];
         memset(dp,0,sizeof(dp));
         dp[0]=1;
         for(int i=1;i<n+1;i++)
            for(int j=sum;j>=nums[i-1];j--)
                dp[j]+=dp[j-nums[i-1]];
        return dp[sum];
    }
};

完全背包问题

与0-1背包不同的一点在于对于每个物品,不是取(1)或者不取(0)两种状态,而是可以取0,1,2…任意次,其动态转移方程满足:
dp[i] [j]=max(dp[i-1] [j-k×w[i]]+k×v[i]),其中 0 ≤ k×w[i] ≤ j
可以发现,当k只能取0,1时的特例就是简单的0-1背包问题,这个可以进一步化简,考虑当该物品不加入背包时,dp[i][j]取决于dp[i-1][j],而当选择加入时,该物品可能已经加入了,dp[i][j-w[i]]≥dp[i-1][j-w[i]]必然成立,所以动态转移方程:
dp[i][j]=max(dp[i-1][j],dp[i][j-w[i]]+v[i])
这样的话,再看其一维数组的化简,不再像简单的0-1背包那样第二层循环进行倒序了,这次需要正序,因为需要得到dp[i][j-w[i]]+v[i]),之前倒序是因为需要保留dp[i-1][j-w[i]]+v[i])。

#include <bits/stdc++.h>

using namespace std;
 //完全背包,可以重复选取物品。
int main()
{
    
    
    int m,n,w[35],v[35];
    cin>>m>>n;  //背包上限m,n个物品
    for(int i=0;i<n;i++)  cin>>w[i]>>v[i];
    int dp[m+1];
    memset(dp,0,sizeof(dp));
    for(int i=1;i<n+1;i++)
        for(int j=w[i-1];j<m+1;j++)
           dp[j]=max(dp[j],dp[j-w[i-1]]+v[i-1]);  //这条语句和简单0-1背包一样
    cout<<"max="<<dp[m];
          //通过与一维数组形式的简单0-1背包对比可以发现只有第二层for循环不一样,其它都一样
    return 0;
} 

LeetCode322. 零钱兑换

这题是反向,背包容量固定,求装满背包的最小物品个数,物品可以重复。
每个硬币的面额coins[i],即物品价值v[i],面额各不相同的硬币有n个,背包容量amount,即C。
假设dp[i][j]表示使用前i种硬币装满容量为j的背包所需的最少硬币数,动态转移方程为:dp[i][j]=min(dp[i-1][j],dp[i][j-coins[i-1]]+1)
此外,我们取的是最小个数,所以对二维数组dp初始化时,第一行dp[0][j]的初始化应尽可能无穷大,表示无法取得,但是dp[0][0]是可以取得的,且dp[0][0]=0。
一维数组进行空间优化: 采用dp[count+1],dp[0]=0。别忘了第二层循环背包容量从小到大采用正序。

class Solution {
    
    
public:
    int coinChange(vector<int>& coins, int amount) {
    
    
        int MAX=0x3f3f3f;
        int n=coins.size();
        vector<int> dp(amount+1,MAX);
        dp[0]=0;
        for(int i=1;i<n+1;i++)
            for(int j=coins[i-1];j<amount+1;j++) //正序
                dp[j]=min(dp[j],dp[j-coins[i-1]]+1);
        if(dp[amount]!=MAX)  return dp[amount];
        else  return -1;
    }
};

猜你喜欢

转载自blog.csdn.net/HangHug_L/article/details/108491601