《代码随想录》刷题打卡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];
    }
};
相关推荐
薛定e的猫咪1 小时前
从因果视角解决多智能体协作:细读 SCIC 算法
算法
OuO-21 小时前
笔试强训 Day 43:kotori 和抽卡(二)、ruby 和薯条、循环汉诺塔
数学·前缀和·二分查找·动态规划·排序·滑动窗口·循环汉诺塔
c238561 小时前
《算法武林谱:四大排序神功与二分寻宝术全解》
数据结构·算法·排序算法
一米阳光86611 小时前
软考(中级)软件设计师核心笔记(9)算法——时间复杂度与空间复杂度、查找算法、排序算法
笔记·算法·职场发展·软考·软件设计师·中级职称
Mem0rin1 小时前
二分查找:左右边界
数据结构·算法
大明者省11 小时前
WSL2 Ubuntu22.04 GPU训练环境配置指南
人工智能·算法·计算机视觉
白狐_79813 小时前
408数据结构第8章:排序②——性质对比秒杀、场景选择与外部排序
java·数据结构·算法
zander25813 小时前
LeetCode 84:柱状图中的最大矩形——单调栈如何确定左右边界
java·数据结构·算法
Shell运维手记13 小时前
Linux 常用基础命令学习笔记
linux·运维·笔记·学习·算法·github