《代码随想录》刷题打卡day31:动态规划-背包问题part02

文章目录

【1049.最后一块石头的重量II】

思路:

本题其实是尽量让石头分成重量相同的两堆(尽可能相同),相撞之后剩下的石头就是最小的。

一堆的石头重量是sum,那么我们就尽可能拼成重量为 sum / 2 的石头堆。 这样剩下的石头堆也是尽可能接近 sum/2 的重量。 那么此时问题就是有一堆石头,每个石头都有自己的重量,是否可以装满最大重量为 sum / 2的背包。

cpp 复制代码
class Solution {
public:
    int lastStoneWeightII(vector<int>& stones) {
        int sum = 0;
        for(int i = 0; i < stones.size(); i++){
            sum += stones[i];
        }
        if (sum == 1) return 1;
        int target = sum / 2;
        // dp[target]表示容量为target的背包最多可以装多少重量石头
        vector<int> dp(15001 , 0);
        for(int i = 0; i < stones.size(); i++){
            for(int j = target; j >= stones[i]; j--){
                dp[j] = max(dp[j], dp[j-stones[i]] + stones[i]);
              // 在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的。
            }
        }
        return sum - dp[target] - dp[target];
    }
};
【494.目标和】

思路:

二维dp数组:

假设加法的总和为x,那么减法对应的总和就是sum - x。

所以我们要求的是 x - (sum - x) = target

x = (target + sum) / 2

此时问题就转化为,用nums装满容量为x的背包,有几种方法。

这里的x,就是bagSize,也就是我们后面要求的背包容量。

看到(target + sum) / 2 应该担心计算的过程中向下取整有没有影响。

且这么担心是有道理的,例如sum是5,target是2 的话其实就是无解的,所以:

cpp 复制代码
(C++代码中,输入的S 就是题目描述的 target)
if ((target + sum) % 2 == 1) return 0; // 此时没有方案

同时如果target 的绝对值已经大于sum,那么也是没有方案的。

cpp 复制代码
if (abs(target) > sum) return 0; // 此时没有方案

因为每个物品(题目中的1)只用一次!

这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少。

本题则是装满有几种方法。其实这就是一个组合问题了。


  1. 确定dp数组及下标的含义:

dpi j:使用下标为0,i的numsi能够凑满j(包括j)这么大容量的包,有dpi j这么多种方法。

  1. 确定递推公式:

抽象化如下:

  • 不放物品i:即背包容量为j,里面不放物品i,装满有dpi - 1 j中方法。
  • 放物品i: 即:先空出物品i的容量,背包容量为(j - 物品i容量),放满背包有 dpi - 1 j - 物品i容量 种方法。

本题中,物品i的容量是numsi,价值也是numsi。

递推公式:dpi j = dpi - 1 j + dpi - 1 j - nums\[i];

看到这个递推公式,我们应该注意到,j - nums[i] 作为数组下标,如果 j - nums[i] 小于零呢?

说明背包容量装不下 物品i,所以此时装满背包的方法值 等于 不放物品i的装满背包的方法,即:dpi j = dpi - 1 j;

所以递推公式:

cpp 复制代码
if (nums[i] > j) dp[i][j] = dp[i - 1][j]; 
else dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];
  1. dp数组如何初始化:

求解dpi j是有其左上方和上方推出,因此二维数组的最上行 和最左列必须初始化。

关于dp0 0的值,在上面的递推公式讲解中已经讲过,装满背包容量为0 的方法数量是1,即 放0件物品。

那么最上行dp0 j 如何初始化呢?

dp0 j:只放物品0, 把容量为j的背包填满有几种方法。

只有背包容量为 物品0 的容量的时候,方法为1,正好装满。

其他情况下,要不是装不满,要不是装不下。

所以初始化:dp0 nums\[0] = 1 ,其他均为0 。

表格最左列也要初始化,dpi 0 : 背包容量为0, 放物品0 到 物品i,装满有几种方法。

都是有一种方法,就是放0件物品。

即 dpi 0 = 1

但这里有例外,就是如果物品数值就是0呢?

如果有两个物品,物品0为0, 物品1为0,装满背包容量为0的方法有几种。

  • 放0件物品
  • 放物品0
  • 放物品1
  • 放物品0 和 物品1

此时是有4种方法。

其实就是算数组里有t个0,然后按照组合数量求,即 2^t 。

初始化如下:

cpp 复制代码
int numZero = 0;
for (int i = 0; i < nums.size(); i++) {
    if (nums[i] == 0) numZero++;
    dp[i][0] = (int) pow(2.0, numZero);
}
  1. 确定遍历顺序

明确递推方向时,我们知道当前值 是由上方 和左上方推出。

那么我们的遍历顺序一定是从上到下,从左到右。

因为只有这样,我们才能基于之前的数值做推导。

先从上到下 ,再从左到右遍历,例如这样:

cpp 复制代码
for (int i = 1; i < nums.size(); i++) { // 行,遍历物品
    for (int j = 0; j <= bagSize; j++) { // 列,遍历背包
    }
}

先从左到右,再从上到下,例如这样:

cpp 复制代码
for (int j = 0; j <= bagSize; j++) { // 列,遍历背包
    for (int i = 1; i < nums.size(); i++) { // 行,遍历物品
    }
}

以上两种遍历都可以! (但仅针对二维DP数组是这样的)

  1. 举例推导dp数组

代码解答:

cpp 复制代码
// 二维dp解法
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i++) sum += nums[i];
        if(abs(target) > sum) return 0; // 此时没有方案
        if((target + sum) % 2 == 1) return 0; // 此时没有方案
        int bagSize = (target + sum) / 2;
        /* 
        假设加法的总和为x,那么减法对应的总和就是sum - x。
        所以我们要求的是 x - (sum - x) = target
        x = (target + sum) / 2
        此时问题就转化为,用nums装满容量为x的背包,有几种方法。
        */
        vector<vector<int>> dp(nums.size(), vector<int>(bagSize + 1, 0));

        // 初始化最上行
        if(nums[0] <= bagSize) dp[0][nums[0]] = 1;
        
        //初始化最左列
        dp[0][0] = 1;

        int numZero = 0;
        for(int i = 0; i < nums.size(); i++){
            if(nums[i] == 0) numZero++;
            dp[i][0] = (int) pow(2.0, numZero);
        }

        // 以下遍历顺序可以颠倒
        for(int i = 1; i < nums.size(); i++){   // 行,遍历物品
            for(int j = 0; j <= bagSize; j++){  // 列, 遍历背包容量
                if(nums[i] > j) dp[i][j] = dp[i-1][j];
                else dp[i][j] = dp[i-1][j] + dp[i-1][j-nums[i]];
            }
        }
        return dp[nums.size()-1][bagSize];
    }
};

一维dp数组:
  1. 确定dp数组及下标含义:

将二维dp数组压缩成一维dp数组,即滚动数组,原理是一样的,即重复利用每一行的数值。

既然是重复利用每一行,就是将二维数组压缩成一行。

dpi j 去掉行的维度,即 dpj,表示:填满j(包括j)这么大容积的包,有dpj种方法。

  1. 确定递推公式

二维DP数组递推公式: dp[i][j] = dp[i - 1][j] + dp[i - 1][j - nums[i]];

去掉维度i 之后,递推公式:dp[j] = dp[j] + dp[j - nums[i]] ,即:dp[j] += dp[j - nums[i]]

这个公式在后面在背包解决排列组合问题的时候还会用到!

  1. dp数组如何初始化:

在上面二维dp数组中,我们讲解过 dp0 0 初始为1,这里dp0 同样初始为1 ,即装满背包为0的方法有一种,放0件物品。

  1. 确定递推顺序:

和前面的一维dp数组方法一样,遍历物品放在外循环,遍历背包在内循环,且内循环倒序(为了保证物品只使用一次)。

  1. 举例推导dp数组

代码解答:

cpp 复制代码
// 一维dp解法
class Solution {
public:
    int findTargetSumWays(vector<int>& nums, int target) {
        int sum = 0;
        for(int i = 0; i < nums.size(); i++) sum += nums[i];
        if(abs(target) > sum) return 0;
        if((target + sum) % 2 == 1) return 0;
        int bagSize = (target + sum) / 2;
        vector<int>dp(bagSize + 1, 0);
        // dp[j],表示:填满j(包括j)这么大容积的包,有dp[j]种方法。
        dp[0] = 1;
        for(int i = 0; i < nums.size(); i++){
            for(int j = bagSize; j >= nums[i]; j--){
                dp[j] += dp[j - nums[i]];
            }
        }
        return dp[bagSize];
    }
};
【474.一和零】

思路:

多重背包是每个物品,数量不同的情况。

本题中strs 数组里的元素就是物品,每个物品都是一个!

而m 和 n相当于是一个背包,两个维度的背包。

切勿把m和n混淆为物品了,感觉这是不同数量的物品,那就理解错成是多重背包了。

但本题其实是01背包问题!

只不过这个背包有两个维度,一个是m,一个是n,而不同长度的字符串就是不同大小的待装物品。

  1. 确定dp数组及其下标含义:

dpi j:最多有i个0和j个1的strs的最大子集的大小为dpi j。

  1. 确定递推公式:

dpi j 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。

dpi j 就可以是 dpi - zeroNum j - oneNum + 1。

然后我们在遍历的过程中,取dpi j的最大值。

所以递推公式:dpij = max(dpi j, dpi - zeroNum j - oneNum + 1);

此时可以回想一下01背包的递推公式:dpj = max(dpj, dpj - weight\[i] + valuei);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weighti),字符串本身的个数相当于物品的价值(valuei)。

这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

  1. dp数组如何初始化:

01背包的dp数组初始化为0就可以。

因为物品价值不会是负数,初始为0,保证递推的时候dpi j不会被初始值覆盖。

  1. 确定遍历顺序:

01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!

cpp 复制代码
for (string str : strs) { // 遍历物品
    int oneNum = 0, zeroNum = 0;
    for (char c : str) {
        if (c == '0') zeroNum++;
        else oneNum++;
    }
    for (int i = m; i >= zeroNum; i--) { // 遍历背包容量且从后向前遍历!
        for (int j = n; j >= oneNum; j--) {
            dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
        }
    }
}
  1. 举例推导dp数组

代码:

cpp 复制代码
class Solution {
public:
    int findMaxForm(vector<string>& strs, int m, int n) {
        vector<vector<int>> dp(m + 1, vector<int> (n + 1, 0)); // 默认初始化0
        // dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
        for(string str: strs){ // 外层遍历物品
            int oneNum = 0, zeroNum = 0;
            for(char c: str){
                if(c == '0') zeroNum++;
                else oneNum++;
            }
            for(int i = m; i >= zeroNum; i--){ // 内层两个维度都要倒序遍历
                for(int j = n; j >= oneNum; j--){
                    dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);
                }
            }
        }
        return dp[m][n];
    }
};
相关推荐
旺仔仔仔19 分钟前
机器人项目实战(5):从单次抓取,到接收指令连续做事
算法
旺仔仔仔27 分钟前
机器人项目实战(2):识别物体、提取点云,再到抓取姿态
算法
姜鱼问生29 分钟前
密码明明一样,为什么 GPG 说“错的“?
后端·算法·架构
启观川43 分钟前
数据结构与算法-第 3 章 常用算法-贪心算法
数据结构·笔记·python·算法·贪心算法
对空六课1 小时前
指标口径生效时间的版本查询实现
大数据·服务器·前端·算法·数据分析
对空六课1 小时前
RFM分层边界变化的回溯计算
大数据·服务器·前端·数据库·人工智能·算法·数据分析
一勺思维1 小时前
我为什么把 Agent 的核心逻辑从 LangChain 里搬出来手写
算法
碧海银沙音频科技研究院1 小时前
基于炬芯ATS308X的LE aduio LC3编解码算法功能实现
算法
小蒋观天下1 小时前
厂区人员徘徊游荡判别——基于时序轨迹速度的逗留行为分级算法落地
大数据·人工智能·深度学习·算法·机器学习·ai大模型
程序员清风2 小时前
堆与优先队列:从 Top K 到任务调度
java·数据结构·算法