算法训练Day43 | LeetCode1049. 最后一块石头的重量II(尽可能装最多能装多少); 494. 目标和(求装满背包有多少种方法);474.一和零(给背包容量,装满背包最多有多少个物品)

前言:

算法训练系列是做《代码随想录》一刷,个人的学习笔记和详细的解题思路,总共会有60篇博客来记录,计划用60天的时间刷完。 

内容包括了面试常见的10类题目,分别是:数组,链表,哈希表,字符串,栈与队列,二叉树,回溯算法,贪心算法,动态规划,单调栈。

博客记录结构上分为 思路,代码实现,复杂度分析,思考和收获,四个方面。

如果这个系列的博客可以帮助到读者,就是我最大的开心啦,一起LeetCode一起进步呀;)
 

目录

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

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

 LeetCode494. 目标和

0. 总体思路

方法一:回溯解法

方法二:动态规划

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获

LeetCode474. 一和零

1. 思路

2. 代码实现

3. 复杂度分析

4. 思考与收获


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

链接:1049. 最后一块石头的重量 II - 力扣(LeetCode)

1. 思路

本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了,和前一道题 9.9 分割等和子集 很像;

本题物品的重量为stones[i],物品的价值也为stones[i];对应着01背包里的物品重量weight[i]和 物品价值value[i]。

接下来进行动规五步曲:

1.1. 确定dp数组以及下标的含义

dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多能装的石头的重量和;

1.2 确定递推公式

01背包的递推公式为:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

本题则是:dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);

  • 一些同学可能看到这dp[j - stones[i]] + stones[i]中 又有- stones[i] 又有+stones[i],看着有点晕乎;
  • 还是要牢记dp[j]的含义,要知道dp[j - stones[i]]为 容量为j - stones[i]的背包最大所背重量;

1.3 dp数组如何初始化

接下来就是如何初始化dp[j]呢,因为重量都不会是负数,所以dp[j]都初始化为0就可以了,这样在递归公式dp[j] = max(dp[j], dp[j - stones[i]] + stones[i]);中dp[j]才不会初始值所覆盖;

dp数组的长度怎么考虑呢?

  • 既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。而我们要求的target其实只是最大重量的一半,所以dp数组开到15000大小就可以了;
  • 当然也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小;

1.4 确定遍历顺序

如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

1.5 举例推导dp数组

举例,输入:[2,4,1,1],此时target = (2 + 4 + 1 + 1)/2 = 4 ,dp数组状态图如下:

算法训练Day43 | LeetCode1049. 最后一块石头的重量II(尽可能装最多能装多少); 494. 目标和(求装满背包有多少种方法);474.一和零(给背包容量,装满背包最多有多少个物品)_第1张图片

最后dp[target]里是容量为target的背包所能背的最大重量。

那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target];**在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的;**那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

2. 代码实现

# time:O(n*target);space:O(target)
class Solution(object):
    def lastStoneWeightII(self, stones):
        """
        :type stones: List[int]
        :rtype: int
        """
        # 初始化
        target  = sum(stones)//2
        dp = [0]*(target+1)
        # 遍历顺序
        for i in range(len(stones)):
            for j in range(target,stones[i]-1,-1):
                # 递推公式
                dp[j] = max(dp[j],dp[j-stones[i]]+stones[i])
        return sum(stones) - dp[target] -dp[target]

3. 复杂度分析

  • 时间复杂度:O(n*target)

    其中n为数组的长度(石头个数),target是整个数组的元素和的一半;需要计算出所有的状态,每个状态计算的时间复杂度为O(1);

  • 空间复杂度:O(target)

    其中 target是整个数组的元素和的一半;空间复杂度取决于dp数组,数组长度就是target+1;

4. 思考与收获

  1. 本题和9.9分割等和子集几乎是一样的,只是最后返回的值的处理方式不同,9.9相当于是求背包是否可以正好装满,而本题是求背包最多能装多少;
  2. 难点是把这个问题转换为 尽可能将石头分成相等重量的两堆,进而转换成求背包容量为sum(target)//2 的情况下,背包能放下的最大重量和是多少。

Reference: 代码随想录 (programmercarl.com)

本题学习时间:30分钟。


 LeetCode494. 目标和

 链接:494. 目标和 - 力扣(LeetCode)

0. 总体思路

这道题目咋眼一看和动态规划背包啥的也没啥关系。

本题要如何使表达式结果为target,可以将数组中的元素分为两个阵营,正号组和负号组;那么就一定有 left组合 - right组合 = target;而left + right = sum;所以说:left = (target + sum)/2 ;

target是固定的,sum是固定的,left就可以求出来;此时问题就是在集合nums中找出和为left的组合有多少种。

方法一:回溯解法

(二刷再看)

在回溯算法系列中,一起学过这道题目**回溯算法:39. 组合总和 (opens new window)**的录友应该感觉很熟悉,这不就是组合总和问题么?

此时可以套组合总和的回溯法代码,几乎不用改动。

当然,也可以转变成序列区间选+ 或者 -,使用回溯法,那就是另一个解法;我也把代码给出来吧,大家可以了解一下,回溯的解法,以下是本题转变为组合总和问题的回溯法代码:

class Solution {
private:
    vector> result;
    vector path;
    void backtracking(vector& candidates, int target, int sum, int startIndex) {
        if (sum == target) {
            result.push_back(path);
        }
        // 如果 sum + candidates[i] > target 就终止遍历
        for (int i = startIndex; i < candidates.size() && sum + candidates[i] <= target; i++) {
            sum += candidates[i];
            path.push_back(candidates[i]);
            backtracking(candidates, target, sum, i + 1);
            sum -= candidates[i];
            path.pop_back();

        }
    }
public:
    int findTargetSumWays(vector& nums, int S) {
        int sum = 0;
        for (int i = 0; i < nums.size(); i++) sum += nums[i];
        if (S > sum) return 0; // 此时没有方案
        if ((S + sum) % 2) return 0; // 此时没有方案,两个int相加的时候要各位小心数值溢出的问题
        int bagSize = (S + sum) / 2; // 转变为组合总和问题,bagsize就是要求的和

        // 以下为回溯法代码
        result.clear();
        path.clear();
        sort(nums.begin(), nums.end()); // 需要排序
        backtracking(nums, bagSize, 0, 0);
        return result.size();
    }
};

以上代码还可以进一步优化,使用记忆化回溯。

方法二:动态规划

1. 思路

x = (target + sum) / 2;此时问题就转化为,装满容量为x背包,有几种方法

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

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

这么担心就对了,例如sum 是5,S是2的话其实就是无解的,所以:

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

class Solution:
  def findTargetSumWays(self, nums: List[int], target: int) -> int:
      sumValue = sum(nums)
# 注意边界条件为 
# target>sumValue or target<-sumValue or (sumValue + target) % 2 == 1
      if abs(target) > sumValue or (sumValue + target) % 2 == 1: 
					return 0

再回归到01背包问题,为什么是01背包呢?

因为每个物品(题目中的1)只用一次!这次和之前遇到的背包问题不一样了,之前都是求容量为j的背包,最多能装多少;本题则是装满有几种方法。其实这就是一个组合问题了。

1.1 确定dp数组以及下标的含义

dp[j] 表示:填满j(包括j)这么大容积的包,有dp[j]种方法;

1.2 1. 确定递推公式

有哪些来源可以推出dp[j]呢?

只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。

例如:dp[j],j 为5,

  • 已经有一个1(nums[i]) 的话,有 dp[4]种方法 凑成 容量为5的背包。
  • 已经有一个2(nums[i]) 的话,有 dp[3]种方法 凑成 容量为5的背包。
  • 已经有一个3(nums[i]) 的话,有 dp[2]中方法 凑成 容量为5的背包
  • 已经有一个4(nums[i]) 的话,有 dp[1]中方法 凑成 容量为5的背包
  • 已经有一个5 (nums[i])的话,有 dp[0]中方法 凑成 容量为5的背包

那么凑整dp[5]有多少方法呢,也就是把 所有的 dp[j - nums[i]] 累加起来。

所以求组合类问题的公式,都是类似这种:

 dp[j] += dp[j - nums[i]]

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

1.3 dp数组如何初始化

  • dp[0]

    从递归公式可以看出,在初始化的时候dp[0] 一定要初始化为1,因为dp[0]是在公式中一切递推结果的起源,如果dp[0]是0的话,递归结果将都是0;这里有录友可能认为从dp数组定义来说 dp[0] 应该是0,也有录友认为dp[0]应该是1;其实不要硬去解释它的含义,咱就把 dp[0]的情况带入本题看看就是应该等于多少。

    如果数组[0] ,target = 0,那么 bagSize = (target + sum) / 2 = 0。 dp[0]也应该是1, 也就是说给数组里的元素 0 前面无论放加法还是减法,都是 1 种方法。所以本题我们应该初始化 dp[0] 为 1。

    可能有同学想了,那 如果是 数组[0,0,0,0,0] target = 0 呢。其实 此时最终的dp[0] = 32,也就是这五个零 子集的所有组合情况,但此dp[0]非彼dp[0],dp[0]能算出32,其基础是因为dp[0] = 1 累加起来的。

  • dp[非零下标]

    dp[j]其他下标对应的数值也应该初始化为0,从递归公式也可以看出,dp[j]要保证是0的初始值,才能正确的由dp[j - nums[i]]推导出来。

1.4 确定遍历顺序

我们讲过对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

1.5 举例推导dp数组

输入:nums: [1, 1, 1, 1, 1], S: 3

bagSize = (S + sum) / 2 = (3 + 5) / 2 = 4

dp数组状态变化如下:

算法训练Day43 | LeetCode1049. 最后一块石头的重量II(尽可能装最多能装多少); 494. 目标和(求装满背包有多少种方法);474.一和零(给背包容量,装满背包最多有多少个物品)_第2张图片

2. 代码实现

# 动态规划-01背包应用
# time:O(N*left);space:O(left)
class Solution(object):
    def findTargetSumWays(self, nums, target):
        """
        :type nums: List[int]
        :type target: int
        :rtype: int
        """
        total = sum(nums)
        # 这里要写abs(target)>total;Example:[100],-200
        if abs(target)>total or (total+target)%2==1:
            return 0
        bagSize = (total+target)//2
        dp = [0]*(bagSize+1)
        dp[0] = 1
        for i in range(len(nums)):
            for j in range(bagSize,nums[i]-1,-1):
                dp[j] += dp[j-nums[i]]
        return dp[bagSize]

3. 复杂度分析

  • 时间复杂度:O(n × m);

    其中n为整数个数,m为背包容量,背包问题中有两层for循环;

  • 空间复杂度:O(m)

    其中m为背包容量;

4. 思考与收获

  1. 代码中边界条件要记得判断abs(target)>total;因为可能出现这种例子:Example:[100],-200;

  2. 此时想起,我们之前讲过的**回溯算法:39. 组合总和 (opens new window)是不是应该也可以用dp来做啊?是的,如果仅仅是求个数的话,就可以用dp,但回溯算法:39. 组合总和 (opens new window)**要求的是把所有组合列出来,还是要使用回溯法爆搜的;

  3. 本题还是有点难度,大家也可以记住,在求装满背包有几种方法的情况下,递推公式一般为:

    dp[j] += dp[j - nums[i]]
    

    后面我们在讲解完全背包的时候,还会用到这个递推公式!

Reference:代码随想录 (programmercarl.com)

本题学习时间:80分钟。


LeetCode474. 一和零

链接:474. 一和零 - 力扣(LeetCode) 

1. 思路

这道题目,还是比较难的,也有点像程序员自己给自己出个脑筋急转弯,程序员何苦为难程序员呢。

来说题,本题不少同学会认为是多重背包,一些题解也是这么写的。

其实本题并不是多重背包,再来看一下这个图,捋清几种背包的关系

算法训练Day43 | LeetCode1049. 最后一块石头的重量II(尽可能装最多能装多少); 494. 目标和(求装满背包有多少种方法);474.一和零(给背包容量,装满背包最多有多少个物品)_第3张图片

多重背包是每个物品,数量不同的情况。本题中strs 数组里的元素就是物品,每个物品都是一个!而m 和 n相当于是一个背包,两个维度的背包。 理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。但本题其实是01背包问题!这不过这个背包有两个维度,一个是m 一个是n,而不同长度的字符串就是不同大小的待装物品。

1.1 确定dp数组(dp table)以及下标的含义

dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j];

1.2 确定递推公式

dp[i][j] 可以由前一个strs里的字符串推导出来,strs里的字符串有zeroNum个0,oneNum个1。dp[i][j] 就可以是 dp[i - zeroNum][j - oneNum] + 1;

然后我们在遍历的过程中,取dp[i][j]的最大值,所以递推公式:

dp[i][j] = max(dp[i][j], dp[i - zeroNum][j - oneNum] + 1);

此时大家可以回想一下01背包的递推公式:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]);

对比一下就会发现,字符串的zeroNum和oneNum相当于物品的重量(weight[i]),字符串本身的个数相当于物品的价值(value[i]);这就是一个典型的01背包! 只不过物品的重量有了两个维度而已。

1.3 dp数组如何初始化

首先dp[0][0]肯定只能为0;然后非零下标反正都是要覆盖的,因为递推公式中需要和自己比,,保证递推的时候dp[i][j]不会被初始值覆盖,所以说也全部初始化为0;

1.4 确定遍历顺序

一维的01背包一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!物品就是strs里的字符串,背包容量就是题目描述中的m和n;代码如下:

for str in strs:
  ones = str.count('1')
  zeros = str.count('0')
  # 遍历背包容量且从后向前遍历!
  for i in range(m, zeros - 1, -1):
      for j in range(n, ones - 1, -1):
          dp[i][j] = max(dp[i][j], dp[i - zeros][j - ones] + 1)

遍历背包容量的两层for循环先后循序有没有什么讲究?

没讲究,都是物品重量的一个维度,先遍历那个都行!

1.5 举例推导dp数组

以输入:["10","0001","111001","1","0"],m = 3,n = 3为例

最后dp数组的状态如下所示:

算法训练Day43 | LeetCode1049. 最后一块石头的重量II(尽可能装最多能装多少); 494. 目标和(求装满背包有多少种方法);474.一和零(给背包容量,装满背包最多有多少个物品)_第4张图片

2. 代码实现

# time:O(N*m*n);space:O(m*n)
# step1.确定dp数组含义:dp[i][j]表示容量为i和j的容器,最多可以放下多少个元素
# step2. 递推公式: dp[i][j] = max(dp[i][j],dp[i-zeroNum][j-OneNum])
# step3. 初始化dp[0][0] = 0
# step4. 遍历顺序,外层遍历物品,数组中的每个元素,内层逆序遍历两个维度的背包容量
# step5. 打印dp,debug
class Solution(object):
    def findMaxForm(self, strs, m, n):
        """
        :type strs: List[str]
        :type m: int
        :type n: int
        :rtype: int
        """
        dp = [[0]*(n+1) for _ in range(m+1)]
        # 遍历物品
        for item in strs:
            zeroNum = item.count("0")
            OneNum = item.count("1")
            # 遍历背包容量且从后向前遍历
            for i in range(m,zeroNum-1,-1):
                for j in range(n,OneNum-1,-1):
                    dp[i][j] = max(dp[i][j],dp[i-zeroNum][j-OneNum]+1)
        return dp[m][n]

3. 复杂度分析

  • 时间复杂度:O(Nmn)

    其中N为数组strs的长度,m和n为题意中给的0和1的元素个数;

  • 空间复杂度:O(m*n)

    其中m和n为题意中给的0和1的元素个数;因为dp数组的大小为(m+1)*(n+1);

4. 思考与收获

  1. 总结01背包问题在不同维度上的应用:
    • 纯01背包:求 给定背包容量 装满背包 的最大价值是多少;
    • 分割等和子集:求 给定背包容量,能不能装满这个背包;
    • 最后一块石头的重量II:求 给定背包容量,尽可能装,最多能装多少;
    • 目标和:求 给定背包容量,装满背包有多少种方法;
    • 本题 一和零:求 给定背包容量,装满背包最多有多少个物品。

Reference:代码随想录 (programmercarl.com)

本题学习时间:60分钟。


本篇学习时间3小时,总结字数7000+;针对01背包的不同层面的应用举了典型的题目,“最后一块石头的重量”是给定背包容量,尽可能装物品,最多能装多少;“目标和”是给定背包容量,问装满背包有多少种方法;“一和零”是给定背包容量,装满背包最多可以有多少物品。(求推荐!)

你可能感兴趣的:(代码随想录训练营,算法,leetcode,python,动态规划,职场和发展)