代码随想录算法训练营第四十四天 | 1049.最后一块石头的重量、494.目标和、474.一和零

目录

1049.最后一块石头的重量

 思路

代码

494.目标和

思路

代码

 474.一和零

思路

代码


1049.最后一块石头的重量

本题就和 昨天的 416. 分割等和子集 很像了,可以尝试先自己思考做一做。
 思路

        一开始我根本就看不懂,真的看着同班的学霸算法课考试框框写完,帅气提交离场都给我看傻了,分分钟就能写出来提交真的牛掰。这道题没思路说明你和我一样没有悟到他的本质:本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。

        这一下就明朗的啊,和昨天的分割等和子集不是一样吗,背包的容量就是总容量的一半。

        放上Carl哥的动态规划五部曲

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

        dp[j]表示容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]

        可以回忆一下01背包中,dp[j]的含义,容量为j的背包,最多可以装的价值为 dp[j]。

相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]”

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]的含义。

3.dp数组如何初始化

        既然 dp[j]中的j表示容量,那么最大容量(重量)是多少呢,就是所有石头的重量和。

因为提示中给出1 <= stones.length <= 30,1 <= stones[i] <= 1000,所以最大重量就是30 * 1000 。

        而我们要求的target其实只是最大重量的一半,所以dp数组开到15000大小就可以了。

当然也可以把石头遍历一遍,计算出石头总重量 然后除2,得到dp数组的大小。

        我这里就直接用15000了。

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

4.确定遍历顺序

        在动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中就已经说明:如果使用一维dp数组,物品遍历的for循环放在外层,遍历背包的for循环放在内层,且内层for循环倒序遍历!

5.举例推导dp数组

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

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

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

        那么分成两堆石头,一堆石头的总重量是dp[target],另一堆就是sum - dp[target]。

        在计算target的时候,target = sum / 2 因为是向下取整,所以sum - dp[target] 一定是大于等于dp[target]的

        那么相撞之后剩下的最小石头重量就是 (sum - dp[target]) - dp[target]。

代码
class Solution:
    def lastStoneWeightII(self, stones: List[int]) -> int:
        dp = [0] * 15001
        total_sum = sum(stones)
        target = total_sum // 2

        for stone in stones:  # 遍历物品
            for j in range(target, stone - 1, -1):  # 遍历背包
                dp[j] = max(dp[j], dp[j - stone] + stone)

        return total_sum - dp[target] - dp[target]

494.目标和

大家重点理解 递推公式:dp[j] += dp[j - nums[i]],这个公式后面的题目 我们还会用到。
思路

        这道题我觉得更多是前面的题目分析比较看你的水平,说白了就是看你的数学推导能力。

        我们把这些数分成两堆left和right,一堆是加正号,另一堆是加负号的。那么就有

        left-right=target

        又因为  left+right=sum

        所以  right=sum-left

        我们可以推导出  left-(sum-left)=target

        即  left=(target+sum) / 2

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

        直接上Carl的动态规划五部曲

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

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

        其实也可以使用二维dp数组来求解本题,dp[i][j]:使用 下标为[0, i]的nums[i]能够凑满j(包括j)这么大容量的包,有dp[i][j]种方法。

下面我都是统一使用一维数组进行讲解, 二维降为一维(滚动数组),其实就是上一层拷贝下来,这个我在动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)也有介绍。

确定递推公式

        有哪些来源可以推出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]]

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

dp数组如何初始化

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

确定遍历顺序

        在动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲过对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。

举例推导dp数组

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

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

        dp数组状态变化如下:

代码
class Solution:
    def findTargetSumWays(self, nums: List[int], target: int) -> int:
        total_sum = sum(nums)  # 计算nums的总和
        if abs(target) > total_sum:
            return 0  # 此时没有方案
        if (target + total_sum) % 2 == 1:
            return 0  # 此时没有方案
        target_sum = (target + total_sum) // 2  # 目标和

        # 创建二维动态规划数组,行表示选取的元素数量,列表示累加和
        dp = [[0] * (target_sum + 1) for _ in range(len(nums) + 1)]

        # 初始化状态
        dp[0][0] = 1

        # 动态规划过程
        for i in range(1, len(nums) + 1):
            for j in range(target_sum + 1):
                dp[i][j] = dp[i - 1][j]  # 不选取当前元素
                if j >= nums[i - 1]:
                    dp[i][j] += dp[i - 1][j - nums[i - 1]]  # 选取当前元素

        return dp[len(nums)][target_sum]  # 返回达到目标和的方案数


 474.一和零

通过这道题目,大家先粗略了解, 01背包,完全背包,多重背包的区别,不过不用细扣,因为后面 对于 完全背包,多重背包 还有单独讲解。
思路

        大家学算法要坚持学,能坚持就要尽量坚持,我之前因为期末拉下了太多天没刷,结果今天要算法期末考试了想着算道练练手结果一点都不会写了。。。

        这道题是多重背包的类型题,之前蓝桥杯学的又忘了。。。(每日崩溃1/1)其实代码和01背包差不多,就是把内层遍历背包容量的顺序变成从前向后遍历,其他都差不多的。

代码
class Solution:
    def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
        dp = [[0] * (n + 1) for _ in range(m + 1)]  # 创建二维动态规划数组,初始化为0
        for s in strs:  # 遍历物品
            zeroNum = s.count('0')  # 统计0的个数
            oneNum = len(s) - zeroNum  # 统计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]
  • 7
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值