后续的背包问题的优化:其实二维转一维,和三维转二维都是因为记录了物品的维度,其实只要遍历了所有的物品即可,无需记录,重点是上一个物品得到的结果在给当前物品的使用的时候如何进行。
如果是0-1背包问题,那么是逆序for j in range(amount,-1,-1):
因为 dp[j] = max(dp[j-nums[i]], dp[j]),使用到了第i-1个物品的信息。
但是如果是完全背包问题,那么直接使用正序即可,因为使用的是当前物品的信息—当前物体可重复选择。
for j in range(amount):
dp[j] = max(dp[j-nums[i]], dp[j])
但是为了面试,最好还是先高维,然后再优化,体现进步。
Leetcode常见的背包问题分为三类。
1、组合问题。
2、True、False问题。
3、最大最小问题。
1、组合问题:
377. 组合总和 Ⅳ
494. 目标和
518. 零钱兑换 II
2、True、False问题:
139. 单词拆分
416. 分割等和子集
3、最大最小问题:
474. 一和零
322. 零钱兑换
组合问题公式
dp[i] += dp[i-num]
True、False问题公式
dp[i] = dp[i] or dp[i-num]
最大最小问题公式
dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
以上三组公式是解决对应问题的核心公式。
当然拿到问题后,需要做到以下几个步骤:
1.分析是否为背包问题。
2.是以上三种背包问题中的哪一种。
3.是0-1背包问题还是完全背包问题。也就是题目给的nums数组中的元素是否可以重复使用。
4.如果是组合问题,是否需要考虑元素之间的顺序。需要考虑顺序有顺序的解法,不需要考虑顺序又有对应的解法。
1、组合问题:
- 组合总和 Ⅳ
- 目标和
- 零钱兑换 II
组合问题重要的是与顺序是否有关,与顺序有关的话像下方类似的爬楼梯的例子。无关的话就像零钱兑换的例子。
组合总和 Ⅳ
给定一个由正整数组成且不存在重复数字的数组,找出和为给定目标正整数的组合的个数。
示例:nums = [1, 2, 3], target = 4
所有可能的组合为:
(1, 1, 1, 1)(1, 1, 2)(1, 2, 1)(1, 3)(2, 1, 1)(2, 2)(3, 1)
请注意,顺序不同的序列被视作不同的组合。因此输出为 7。
dp[j] += dp[j-num[i]]
# 该问题可以视为70.爬楼梯问题的升级版本。直接转化为如下问题:
# 假设你正在爬楼梯。需要 n 阶你才能到达楼顶。
# 每次你可以爬num (num in nums)级台阶。你有多少种不同的方法可以爬到楼顶呢?
# 牢记@labuladong东哥的动态规划解题魔咒:
# 1 明确 base case -> 明确「状态」-> 明确「选择」 -> 定义 dp 数组/函数的含义。
# 2 base case: dp[0] = 1: 想要达到第0级,只有不跳这一种方法。
# 3 明确状态: 该问题只有一格状态,就是当前台阶级数,i从0到target。
# 4 明确选择: 选择就是选择跳num级。
# 定义dp数组含义:dp[i]定义:一个人跳台阶,每次可以选择跳num阶(num in nums),他要跳到第i级台阶总共有多少种跳法。显然,跳到第i级台阶的方法数为跳到 dp[i-num] for num in nums的方法数之和,因为他只要跳到第i-num级,再一步跳num级,就可以到第i级了。
class Solution:
def combinationSum4(self, nums: List[int], target: int) -> int:
if not nums:
return 0
dp = []
dp = [0]*(target+1)
dp[0] = 1 # 每次可以上几阶,这里都不上,算一种方法。
for i in range(target+1):
for num in nums:
if i - num >= 0:
dp[i] += dp[i-num] #
return dp[-1]
其实这一题和上一题一模一样~~
零钱兑换 II
解释一下为什么不会出现[1,2]–[2,1]的重复组合情况:
nums[i]是按顺序取的,不会出现取了2再取1的情况,肯定是取了1再继续探究是否取2.
dp[i][j] = dp[i-1][j] + dp[i][j-nums[i]]
dp[i][0] = 1
dp[0][i] = 1
dp[1][j] = dp[0][j] + dp[1][j-nums[i]]
dp[1][1] = dp[0][1] = 1
dp[1][2] = dp[0][2] + dp[1][2-2] = 1 + 1 = 2
dp[1][3] = dp[0][3] + dp[1][3-2] = 1 + 1 = 2
nums: [1, 2]
如果nums是在内循环:
dp[0][i] = 1
dp[i][0] = if i % nums[0] == 0: 1 else: 0 ------ 此时都是1
dp[1][1] = dp[1][0] + dp[1-nums[1]][1] = 1
dp[1][2] = d[0][2] + dp[1][0] = 1 + 1 = 2
dp[1][3] = dp[0][3] + dp[1][1] = 2
给定不同面额的硬币和一个总金额。写出函数来计算可以凑成总金额的硬币组合数。假设每一种面额的硬币有无限个。
示例 1:
输入: amount = 5, coins = [1, 2, 5] 输出: 4
解释: 有四种方式可以凑成总金额:
5=5
5=2+2+1
5=2+1+1+1
5=1+1+1+1+1
示例 2:
输入: amount = 3, coins = [2]
输出: 0
解释: 只用面额2的硬币不能凑成总金额3。
这个就是真正的完全背包问题了,dp[i][j]指的是每个j最大的组合数
dp[i][j]代表使用前i枚硬币可以拼凑出amount为j的最大组合数。
def change(self, amount: int, coins: List[int]) -> int:
# dp[i][j]代表每个amount最大的组合数
if not coins:
if amount == 0:
return 1
else:
return 0
dp = []
for i in range(len(coins)):
dp.append([0]*(amount+1))
for i in range(1, amount+1):
if i % coins[0] == 0:
dp[0][i] = 1
for i in range(len(coins)):
dp[i][0] = 1
for i in range(1, len(coins)):
for j in range(1, amount+1):
if j - coins[i] >= 0:
dp[i][j] = dp[i][j-coins[i]] + dp[i-1][j]
else:
dp[i][j] = dp[i-1][j]
return dp[-1][-1]
二维转一维,优化之后就是dp[i] += dp[i-nums[i]]
class Solution:
def change(self, amount: int, coins: List[int]) -> int:
if not coins:
if amount == 0:
return 1
else:
return 0
dp = [0]*(amount+1)
# 注意一下 basecase, 0的地方是1,虽然解释不同,但是列表可以发现是必须的!!
for i in range(0, amount+1):
if i % coins[0] == 0:
dp[i] = 1
for i in range(1, len(coins)):
for j in range(1,amount+1):
if j - coins[i] >= 0:
dp[j] = dp[j-coins[i]] + dp[j]
return dp[-1]
2、True、False问题:
139. 单词拆分
给定一个非空字符串 s 和一个包含非空单词的列表 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
说明:
拆分时可以重复使用字典中的单词。
你可以假设字典中没有重复的单词。
示例 1:
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以被拆分成 “leet code”。
示例 2:
输入: s = “applepenapple”, wordDict = [“apple”, “pen”]
输出: true
解释: 返回 true 因为 “applepenapple” 可以被拆分成 “apple pen apple”。
注意你可以重复使用字典中的单词。
首先使用DP算法:
初始化 dp=[False],长度为 n+1。n 为字符串长度。dp[i] 表示 ss 的前 i 位是否可以用 wordDict 中的单词表示。
初始化 dp[0]=True 空字符可以被表示。
遍历字符串的所有子串,遍历开始索引 i,遍历区间 [0,n):
遍历结束索引 j,遍历区间 [i+1,n+1):
若 dp[i]=True 且s[i,⋯,j) 在 wordlist 中:dp[j]=True。解释:dp[i]=True 说明 s 的前 i 位可以用 wordDict 表示,则s[i,⋯,j) 出现在 wordDict中,说明 s 的前 j 位可以表示。返回 dp[n]
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp = [0]*(len(s)+1)
dp[0] = 1
for i in range(len(s)):
for j in range(i+1, len(s)+1):
if dp[i] and (s[i:j] in wordDict):
dp[j] = 1
if dp[-1]:
return True
else:
return False
注意:
import functools
@functoolls.lru_cache(None)
装饰器可以实现调用缓存的作用,这个对于重复的调用非常有用,对于一些自顶向下的DP来说非常友好。–另一个博客有进行记录。
回溯的方法:从某一个开始,便不断往后移动,如果无法继续往后,则回溯。
import functools
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
import functools
@functools.lru_cache(None)
def backstrack(s):
if not s:
return True
for j in range(len(s)+1):
if s[0:j] in wordDict:
res = backstrack(s[j:])
if res:
return True
return False
return backstrack(s)
回溯使用的缓存机制有效的case:
#下面的装饰器用在重复的case,来调用缓存还不错:
#“aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaab”
#[“a”,“aa”,“aaa”,“aaaa”,“aaaaa”,“aaaaaa”,“aaaaaaa”,“aaaaaaaa”,“aaaaaaaaa”,“aaaaaaaaaa”]
416. 分割等和子集
给定一个只包含正整数的非空数组。是否可以将这个数组分割成两个子集,使得两个子集的元素和相等。
注意:
每个数组中的元素不会超过 100 数组的大小不会超过 200
示例 1:
输入: [1, 5, 11, 5]
输出: true 解释: 数组可以分割成 [1, 5, 5] 和 [11].
示例 2:
输入: [1, 2, 3, 5]
输出: false 解释: 数组不能分割成两个元素和相等的子集.
分割成两个相等的子集,也就是背包的容量是一定的,现在是要求能否装满,属于0-1背包问题,使用的是True/False来进行解决
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total_sum = sum(nums)
n = len(nums)
if total_sum%2==1:
return False
half_sum = total_sum//2
dp = [[0]*(half_sum+1) for i in range(n)]
### 初始化条件 ###
if nums[0]<=half_sum:
dp[0][nums[0]]=1 # 容量为第一个的时候,肯定能装满
for i in range(n):
dp[i][0]=1 # 容量为0的时候,肯定能装满
for i in range(1,n):
if dp[i][-1] == 1: # 剪枝,只要满足了最后一列,也就是half,提前满足,可以跳出。
return True
for j in range(half_sum+1):
dp[i][j] = dp[i-1][j] # 如果背包装不下
if nums[i]<=j: # 如果背包装得下
dp[i][j] = dp[i-1][j] or dp[i-1][j-nums[i]]
# dp[][:] = dp[1][:] # 注意,不可以dp[1] = dp[2]
if dp[-1][-1]:
return True
else:
return False
转换一维,二维转一维只要注意遍历的顺序逆序就好了。
class Solution:
def canPartition(self, nums: List[int]) -> bool:
total_sum = sum(nums)
n = len(nums)
if total_sum%2==1:
return False
half_sum = total_sum//2
dp = [0]*(half_sum+1)
### 初始化条件 ###
if nums[0]<=half_sum:
dp[nums[0]]=1 # 容量为第一个的时候,肯定能装满
dp[0]=1 # 容量为0的时候,肯定能装满
for i in range(1,n):
if dp[-1] == 1: # 剪枝,只要满足了最后一列,也就是half,提前满足,可以跳出。
return True
for j in range(half_sum, -1, -1):
if nums[i]<=j: # 如果背包装得下
dp[j] = dp[j] or dp[j-nums[i]]
if dp[-1]:
return True
else:
return False
3、最大最小问题:
最大最小问题公式
dp[i] = min(dp[i], dp[i-num]+1)或者dp[i] = max(dp[i], dp[i-num]+1)
474. 一和零
给你一个二进制字符串数组 strs 和两个整数 m 和 n 。
请你找出并返回 strs 的最大子集的大小,该子集中 最多 有 m 个 0 和 n 个 1 。
如果 x 的所有元素也是 y 的元素,集合 x 是集合 y 的 子集 。
示例 1:
输入:strs = [“10”, “0001”, “111001”, “1”, “0”], m = 5, n = 3
输出:4
解释:最多有 5 个 0 和 3 个 1 的最大子集是 {“10”,“0001”,“1”,“0”} ,因此答案是 4 。
其他满足题意但较小的子集包括 {“0001”,“1”} 和 {“10”,“1”,“0”} 。{“111001”} 不满足题意,因为它含 4 个 1 ,大于 n 的值 3 。
示例 2:
输入:strs = [“10”, “0”, “1”], m = 1, n = 1
输出:2
解释:最大的子集是 {“0”, “1”} ,所以答案是 2 。
##这道题除了使用最大最小之外,也使用了二维的数组,二维的0-1背包问题而已
class Solution:
# 三维转二维!!!
# 最大子集,集合里的元素越多越好。不能超过条件,就是背包问题,容量上限就是1和0的个数,每个元素都可以决定添加与否。转移
# dp[i][j][k]代表子集数目
def findMaxForm(self, strs, m, n):
def sum_zo(num):
out_one = 0
out_zero = 0
for i in num:
if i == "0":
out_zero += 1
elif i == "1":
out_one += 1
return out_zero, out_one
if not strs:
return 0
out_zero, out_one = sum_zo(strs[0])
if len(strs) == 1:
if out_zero <= m and out_one <= n:
return 1
else:
return 0
dp1 = []
for j in range(m + 1):
sub = [0]*(n+1)
dp1.append(sub)
if out_zero <= m and out_one <= n:
dp1[out_zero][out_one] = 1
for stri in range(1, len(strs)):
out_zero, out_one = sum_zo(strs[stri])
for j in range(m, -1, -1):
for k in range(n, -1, -1):
if j - out_zero >= 0 and k - out_one >= 0:
dp1[j][k] = max(dp1[j - out_zero][k - out_one] + 1, dp1[j][k])
res = 0
for i in dp1:
if max(i) > res:
res = max(i)
return res
322. 零钱兑换
给定不同面额的硬币 coins 和一个总金额 amount。编写一个函数来计算可以凑成总金额所需的最少的硬币个数。如果没有任何一种硬币组合能组成总金额,返回 -1。
你可以认为每种硬币的数量是无限的。
示例 1:
输入:coins = [1, 2, 5], amount = 11
输出:3
解释:11 = 5 + 5 + 1
示例 2:
输入:coins = [2], amount = 3
输出:-1
示例 3:
输入:coins = [1], amount = 0
输出:0
先弄二维的情况:
注意basecase一定要对,以及边界条件。
# dp[i][j]代表的是前i个物品背包容量为j时,最少的硬币个数
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if not amount:
return 0
if not coins:
return -1
dp = []
for i in range(len(coins)):
dp.append([amount+1] * (amount+1))
for i in range(0, amount+1): #注意0的地方是0,不是的话,会出现问题,后面dp[5][5] = dp[5][0] + 1,basecase一定要确认好!!!!
if i % coins[0] == 0:
dp[0][i] = i // coins[0]
for i in range(1, len(coins)):
for j in range(amount+1):
dp[i][j] = dp[i-1][j]
if j - coins[i] >= 0:
dp[i][j] = min(dp[i][j-coins[i]]+1, dp[i][j])
if dp[-1][-1] == amount+1:
return -1
else:
return dp[-1][-1]
一维,注意完全背包就是不需要逆序,所以遍历的顺序还是和二维是一样的。
class Solution:
def coinChange(self, coins: List[int], amount: int) -> int:
if not amount:
return 0
if not coins:
return -1
dp = [amount+1] * (amount+1)
for i in range(0, amount+1): #注意0的地方是0,不是的话,会出现问题,后面dp[5][5] = dp[5][0] + 1,basecase一定要确认好!!!!
if i % coins[0] == 0:
dp[i] = i // coins[0]
for i in range(1, len(coins)):
for j in range(amount+1):
if j - coins[i] >= 0:
dp[j] = min(dp[j-coins[i]]+1, dp[j])
if dp[-1] == amount+1:
return -1
else:
return dp[-1]