动态规划---背包问题

0-1背包问题

最常见的0-1背包问题,问题描述如下:

有一个装载重量为W的背包和N个商品,每个物品都有重量和价值两个属性,其中第i个商品的重量为wt[i],价值为val[i],现在用这个背包装商品,问最多能装的价值是多少?

在这里插入图片描述
举个例子更直观吧:

高压锅重 4kg,价值300
风扇重   3kg,价值200
皮鞋重   1kg,价值150
背包总重4kg;

在面对一个动态规划时候,往往从以下几个方面分析,会有奇效:

  1. 本题中状态和选择是什么?

这个例子中的状态很显然有两个,一个就是背包的重量W,一个就是供我们选择的物品N;选择就是你要不要把这个物品放入背包中(放入背包或不放入背包)

  1. 本题中dp数组怎么定义呢?

上面我们分析了例子中的状态有2个,背包的重量W,可选择的物品N,也就是说我们需要一个二维的dp数组,dp[N][w]

下面我们画图来理解一下:

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

很显然,我们都能看出来最后一步的最大价值为:

200的风扇 + 150的运动鞋

但是我们的思考过程是什么呢?

选择风扇的话:背包容量还有4-3=1kg,还可以加上一双1kg的运动鞋。
不选择风扇,直接选择4kg高压锅的话,最大价值为300.

根据我们上面的思路就可以得出我们的递推公式:

在这里插入图片描述

有小伙伴可能有疑问,如果我改变一下商品的选择顺序呢?
刚才规定的商品顺序为:运动鞋->高压锅->风扇;
如果我们把顺序改为:风扇->运动鞋->高压锅;
其实可以下去试一下,结果是一样的~

我们来总结一下数组dp的定义:

dp[i][j]表示:对于前i个商品,当前背包容量为j的时候,能存放的最大价值为dp[i][j]
如果没有选择第i个商品,则最大价值就是dp[i - 1][j]
如果选择了第i个商品,则最大价值就为dp[i - 1][j - wt[i - 1]] + value[i - 1];

Notes:因为商品都是从1开始的,所以i-1表示第i个商品的索引

完整代码:

#include<vector>
#include <string>
#include <iostream>
#include <string>

using namespace std;

class Solution
{
public:
    int knapsack(int W,int N,vector<int> &wt,vector<int> &value)
    {
        //初始化+base case
        vector<vector<int>>dp(N + 1,vector<int>(W + 1,0));
        for (int i = 1;i <= N;++i)
        {
            for (int j = 1;j <= W;++j)
            {
                if (j >= wt[i - 1])
                    dp[i][j] = max(dp[i - 1][j],dp[i - 1][j - wt[i - 1]] + value[i - 1]);
                else
                    dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[N][W];
    }
};

int main()
{
    int W = 4,N = 3;
    vector<int> wt = {4,3,1};
    vector<int> value = {300,200,150};
    Solution S;
    int res = S.knapsack(W,N,wt,value);
    cout<<res<<endl;
    return 0;
}

在这里插入图片描述

我们发现的二维数组我们每次计算的时候都是只需要上一行的数字,其他的我们都用不到,所以我们可以用一维空间的数组来记录上一行的值即可,但要记住一维的时候一定要逆序,因为如果不逆顺序,数组前面的值更新以后就会覆盖后面的值,从而发生了重复计算:

#include<vector>
#include <string>
#include <iostream>
#include <string>

using namespace std;
class Solution
{
public:
    int knapsack(int W,int N,vector<int> &wt,vector<int> &value)
    {
        vector<int>dp(W + 1,0);
        for (int i = 1;i <= N;++i)
        {
            for (int j = W;j >= 1;--j)
            {
                if (j >= wt[i - 1])
                    dp[j] = max(dp[j],dp[j - wt[i - 1]] + value[i - 1]);
            }
        }
        return dp[W];
    }
};
int main()
{
    int W = 4,N = 3;
    vector<int> wt = {4,3,1};
    vector<int> value = {300,200,150};
    Solution S;
    int res = S.knapsack(W,N,wt,value);
    cout<<res<<endl;
    return 0;
}

以上就是基本的0-1背包问题,在做题的过程中,基本明确了dp数组的定义,就可以顺理成章的推出递推公式,那么写代码就不在话下了。。。

子集背包问题

LeetCode416:分割等和子集

在这里插入图片描述

这道题眨眼一看和背包有个毛线关系啊,不过可以换一种思路:

可以先对nums数组求和,问题就转换成了,给你一个容量为sum/2的背包,有nums.size()个物品,每个物品的重量为nums[i],问你有没有一种装法,可以把背包装满,如果有返回true,否则返回false;

这样这个问题就转化为0-1背包问题了

接下来我们看dp数组的定义:
此题dp[i][j]表示前i个物品,当前背包重量为j,若dp[i][j] = true,则表示恰好可以装满,否则不能恰好装满

base case很好确定:

dp[N][0]:当背包容量为0的时候,什么都不用装,就相当于满了;dp[N][0] = true;
dp[0][sum / 2]:当背包没有物品,你咋装满?dp[0][sum / 2] = false;

状态转移方程就可以参照0-1背包,根据问题稍作修改即可:

1. 如果不把第i个物品装入背包,那么可不可以恰好装满,取决于上一个状态:dp[i - 1][j]
2. 如果把第i个物品装入背包,那么恰好装满取决于dp[i][j - nums[i - 1]];这里状态方程的意思就是:如果把第i个物品装进去,就看背包剩下的重量j - nums[i - 1]时,能否被刚好装满。

完整代码:

class Solution {
public:
    bool canPartition(vector<int>& nums)
    {
        int sum = 0;
        for (int c : nums)
            sum += c;
        if (sum & 1) //如果sum是奇数,就不用继续了,因为不能等分为2份呀。
            return false;
        //问题转化为背包问题
        //有一个重量为sum/2的背包和nums.size()个物品,每个物品的重量的nums[i]
        //判断有没有一种装法,能够恰好装满背包
        vector<vector<bool >> dp(nums.size() + 1,vector<bool>(sum / 2 + 1,0));
        //base case
        for (int i = 0;i <= nums.size();++i)
            dp[i][0] = true;
        for (int i = 1;i <= nums.size();++i)
        {
            for (int j = 1;j <= sum / 2;++j)
            {
                if (j >= nums[i - 1])
                    dp[i][j] = dp[i - 1][j]|| dp[i - 1][j - nums[i - 1]];
                else
                    dp[i][j] = dp[i - 1][j];
            }
        }
        return dp[nums.size()][sum / 2];
    }
};

在这里插入图片描述

状态压缩后:

class Solution {
public:
    bool canPartition(vector<int> &nums)
    {
        int sum = 0;
        for (int c : nums)
            sum += c;
        if (sum & 1)
            return false;
        vector<bool> dp(sum / 2 + 1,0);
        //base case;
        dp[0] = true;
        for (int i = 0;i < nums.size();++i)
        {
            for (int j = sum / 2 ;j >= 0;--j)
            {
                if (j >= nums[i])
                    dp[j] = dp[j] || dp[j - nums[i]];
            }
        }
        return dp[sum / 2];
    }
};

在这里插入图片描述

完全背包问题

其实完全背包问题和上面两个背包问题的最大区别就是:每个物品的数量无限制。我们来看一道典型的完全背包问题

LeetCode 518:零钱兑换II
在这里插入图片描述

其实这道题还是类似于上面的解题步骤:

  1. 状态和选择
  2. dp数组的定义
  3. 推导状态转移方程

就直接说dp数组的定义吧:

dp[i][j]就表示了使用前i个硬币的价值,想凑到j的金额时,有dp[i][j]中凑法

base case就是dp[0][....] = 0,dp[....][0] = 1;

状态转移方程的思想还是类似于前面的背包问题

1. 如果不用coins[i]这个面值的硬币,dp[i][j]=dp[i-1][j];
2. 如果用conis[i]这个面值的硬币,dp[i][j]=dp[i-1][j-conis[i-1]];这个就是说如果你用conis[i]这个面值的硬币后,就只关心怎么凑出面额为j - coins[i - 1],就好比你已经用面值为2的硬币凑出7块钱,你如果知道了凑出5块钱的方法,再加上你那面值为2的硬币不就成了?

完整代码:


class Solution {
public:
    int change(int amount, vector<int>& coins)
    {
        vector<vector<int>>dp(coins.size() + 1,vector<int>(amount + 1,0));
        //base case;
        for (int i = 0;i <= coins.size();++i)
            dp[i][0] = 1;
        for (int i = 1;i <= coins.size();++i)
        {
            for (int j = 1;j <= amount;++j)
            {
                if (j >= coins[i - 1])
                    dp[i][j] = dp[i - 1][j] + dp[i][j - coins[i - 1]];
                else
                    dp[i][j] = dp[i - 1][j];//继承
            }
        }
        return dp[coins.size()][amount];
    }
};

状态压缩

class Solution {
public:
    int change(int amount, vector<int>& coins)
    {
        vector<int> dp(amount + 1,0);
        dp[0] = 1;
        for (int i = 0;i < coins.size();++i)
        {
            for (int j = coins[i];j <= amount;++j)
                dp[j] += dp[j - coins[i]];
        }
        return dp[amount];
    }
};

再来个零钱兑换I吧

完整代码:


class Solution {
public:
    int coinChange(vector<int>& coins, int amount)
    {
        vector<int> dp(amount + 1,INT_MAX);
        dp[0] = 0;
        for (int i = 0;i < coins.size();++i)
        {
            for (int j = coins[i];j <= amount;++j)
            {
                if (dp[j - coins[i]] != INT_MAX)
                    dp[j] = min(dp[j - coins[i]] + 1,dp[j]);
            }
        }
        return (dp[amount] == INT_MAX) ? -1 : dp[amount];
    }
};

我感觉动态规划是一个很玄学的东西,你如果想出来状态转移方程了,稍加修改就可以写出大体框架了,如果想不出来,能把我想死在电脑面前🤦‍♂️。。。。还是太菜,继续努力吧~~~

如果本文有错误,欢迎大佬指出来,我还在学习的过程中,会虚心改错滴~~~~🙏

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值