Day43 动态规划part05-01背包问题
1049.最后一块石头的重量II
leetcode题目链接:1049. 最后一块石头的重量 II - 力扣(LeetCode)
题意:有一堆石头,用整数数组 stones
表示。其中 stones[i]
表示第 i
块石头的重量。每一回合,从中选出任意两块石头,然后将它们一起粉碎。假设石头的重量分别为 x
和 y
,且 x <= y
。那么粉碎的可能结果如下:
- 如果
x == y
,那么两块石头都会被完全粉碎; - 如果
x != y
,那么重量为x
的石头将会完全粉碎,而重量为y
的石头新重量为y-x
。
最后,最多只会剩下一块 石头。返回此石头 最小的可能重量 。如果没有石头剩下,就返回 0
。
**思路:类似分割等额子集!我们想把这些分为尽可能总和相等的两堆!**本题其实就是尽量让石头分成重量相同的两堆,相撞之后剩下的石头最小,这样就化解成01背包问题了。
一维dp版本:
- dp[j]数组的含义:容量(这里说容量更形象,其实就是重量)为j的背包,最多可以背最大重量为dp[j]。01背包中,dp[j]的含义,容量为j的背包,最多可以装的价值为 dp[j]。相对于 01背包,本题中,石头的重量是 stones[i],石头的价值也是 stones[i] ,可以 “最多可以装的价值为 dp[j]” == “最多可以背的重量为dp[j]”
- **递推公式:dp[j] = max(dp[j], dp[j-weight[i]] + value[i])**前一种是没有选择该物品i的情况,后一种是选择i的情况。本题种就是weight和value都是stone的重量
- 初始化:都初始化成0,要不然可能会在递推的时候把他们覆盖;我们定义一个sum/2的(向下取整)
- 遍历顺序:先for遍历物品;再for遍历背包,而且是从大往小遍历(反向遍历)
- 最后结果是(sum-dp[target])-dp[target], 因为是向下取整所以dp[target]一定是小于等于另外一部分石头的
class Solution:
def lastStoneWeightII(self, stones: List[int]) -> int:
n = len(stones)
sum_all = sum(stones)
target = sum_all // 2
print(sum_all, target)
dp = [0] * (target+1)
for i in range(n): #遍历物品
for j in range(target, stones[i]-1, -1):
# print(j, stones[i]-1)
dp[j] = max(dp[j], dp[j-stones[i]] + stones[i])
print(dp)
return (sum_all - dp[-1]) - dp[-1]
494.目标和
leetcode题目链接:. - 力扣(LeetCode)
题意:给定一个非负整数数组,a1, a2, ..., an, 和一个目标数,S。现在你有两个符号 + 和 -。对于数组中的任意一个整数,你都可以从 + 或 -中选择一个符号添加在前面。返回可以使最终数组和为目标数 S 的所有添加符号的方法数。
示例:
- 输入:nums: [1, 1, 1, 1, 1], S: 3
- 输出:5
思路:假如本题中目标值为target,左集合left为包含左集合元素的总和,右集合right也是同样,那么left + right = sum,同时还希望left - right = target,由此推导出 2 * left = (target + sum)/2,这里target和sum都是固定的,所以可以求出left
此时问题就转化为,装满容量为x的背包,有几种方法。
- dp[j]数组含义:和为j时有多少种组合方式,装满容量为j的背包有有几种方法
- 递推公式:只要搞到nums[i],凑成dp[j]就有dp[j - nums[i]] 种方法。例如
- 已经有一个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[j] += dp[j-nums[i]]
- 初始化:dp[0] = 1因为装满为target = 0的背包有1种组合,要不后序没法递推下去
- 遍历顺序:对于01背包问题一维dp的遍历,nums放在外循环,target在内循环,且内循环倒序。
class Solution:
def findTargetSumWays(self, nums: List[int], target: int) -> int:
sum_all = sum(nums)
if abs(target) > sum_all:
return 0
if (sum_all + target)%2 !=0:
return 0
left = (sum_all + target)//2
dp = [0] * (left+1)
dp[0] = 1
for num in nums:
for j in range(left, num-1, -1):
dp[j] += dp[j-num]
# print(dp)
return dp[left]
# 示例输入:[1,1,1,1,1], target = 3
dp数组:
[1, 1, 0, 0, 0]
[1, 2, 1, 0, 0]
[1, 3, 3, 1, 0]
[1, 4, 6, 4, 1]
[1, 5, 10, 10, 5]
474.一和零
leetcode题目链接:. - 力扣(LeetCode)
题意:给你一个二进制字符串数组 strs 和两个整数 m 和 n 。请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
思路:
多重背包是每个物品,数量不同的情况。
本题中strs 数组里的元素就是物品,每个物品都是一个!而m 和 n相当于是一个背包,两个维度的背包。
理解成多重背包的同学主要是把m和n混淆为物品了,感觉这是不同数量的物品,所以以为是多重背包。但本题其实是01背包问题!
- 确定dp数组(dp table)以及下标的含义:dp[i][j]:最多有i个0和j个1的strs的最大子集的大小为dp[i][j]。
- 递推公式:如果一个str种有a个0和b个1,那么dp[i][j] = max(dp[i][j], dp[i-a][j-b] +1)
- 初始化:因为物品价值不会是负数,初始为0,保证递推的时候dp[i][j]不会被初始值覆盖。
- 确定遍历顺序:01背包为什么一定是外层for循环遍历物品,内层for循环遍历背包容量且从后向前遍历!
总结0-1背包:此时我们讲解了0-1背包的多种应用,
- **纯 0 - 1 背包 (opens new window)**是求 给定背包容量 装满背包 的最大价值是多少。
- **416. 分割等和子集 (opens new window)**是求 给定背包容量,能不能装满这个背包。
- **1049. 最后一块石头的重量 II (opens new window)**是求 给定背包容量,尽可能装,最多能装多少
- **494. 目标和 (opens new window)**是求 给定背包容量,装满背包有多少种方法。
- 本题是求 给定背包容量,装满背包最多有多少个物品。
class Solution:
def findMaxForm(self, strs: List[str], m: int, n: int) -> int:
dp = [[0]*(n+1) for _ in range(m+1)]
for s in strs:
zeros = s.count('0') #掌握这种字符串计算方式
ones = s.count('1')
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)
# print(dp)
return dp[m][n]