139.单词拆分-完全背包问题区分求组合数和排列数
本题可以使用回溯算法进行暴力搜索,但是如何使用动态规划的思路进行求解呢。将字符串可以理解成一个容器,将单词可以当成物品,那么此时问题转化成利用物品能否装满容器的问题。这个时候由于返回的是True或者False,所以在搜索过程中需要将dp数组赋值成True或者False。
动归五步曲:
- dp[i]:长度为i的子串是否能被字典中的单词所组成。
- dp[0]=True
- 递推公式: dp[i] = dp[i] or dp[i-len(j)] and (s[i-len(j):i]==j),看当前状态是否用字典中的字符串进行表示。
- 遍历方式:
- 打印dp数组
思考:
返回什么结果那么dp数组的返回值就要设置成什么结果。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
# 转化成背包问题就是,单词的排列问题,单词的整体长度记作背包的承载重量
# 长度为i的背包,可以用字典中的单词进行拆分。
target = len(s)
dp = [False] * (target + 1) #i的背包有几种装物品的方式
dp[0] = True
for i in range(1,target+1):
for j in wordDict:
if i >= len(j):
dp[i] = dp[i] or dp[i-len(j)] and (s[i-len(j):i]==j)
return dp[target]
多重背包问题-理论基础
多重背包定义:
有N种物品和一个容量为V 的背包。第i种物品最多有Mi件可用,每件耗费的空间是Ci ,价值是Wi 。求解将哪些物品装入背包可使这些物品的耗费的空间 总和不超过背包容量,且价值总和最大。
多重背包非常类似于0-1背包:
每件物品最多有Mi件可用,把Mi件摊开,其实就是一个01背包问题了。
能在0-1背包问题的基础上,把多重背包的逻辑实现即可。
背包问题总结
总结来自于代码随想录
背包递推公式
问能否能装满背包(或者最多装多少):dp[j] = max(dp[j], dp[j - nums[i]] + nums[i]);
动态规划:416.分割等和子集(opens new window)
动态规划:1049.最后一块石头的重量 II(opens new window)
问装满背包有几种方法:dp[j] += dp[j - nums[i]] ,对应题目如下:
动态规划:494.目标和(opens new window)
动态规划:518. 零钱兑换 II(opens new window)
动态规划:377.组合总和Ⅳ(opens new window)
动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
问背包装满最大价值:dp[j] = max(dp[j], dp[j - weight[i]] + value[i]); ,对应题目如下:
动态规划:474.一和零(opens new window)
问装满背包所有物品的最小个数:dp[j] = min(dp[j - coins[i]] + 1, dp[j]); ,对应题目如下:
动态规划:322.零钱兑换(opens new window)
动态规划:279.完全平方数(opens new window)
01背包
在动态规划:关于01背包问题,你该了解这些! (opens new window)中我们讲解二维dp数组01背包先遍历物品还是先遍历背包都是可以的,且第二层for循环是从小到大遍历。
和动态规划:关于01背包问题,你该了解这些!(滚动数组) (opens new window)中,我们讲解一维dp数组01背包只能先遍历物品再遍历背包容量,且第二层for循环是从大到小遍历。
一维dp数组的背包在遍历顺序上和二维dp数组实现的01背包其实是有很大差异的,大家需要注意!
完全背包
如果求组合数就是外层for循环遍历物品,内层for遍历背包。
如果求排列数就是外层for遍历背包,内层for循环遍历物品。
相关题目如下:
求组合数:动态规划:518.零钱兑换II(opens new window)
求排列数:动态规划:377. 组合总和 Ⅳ (opens new window)、动态规划:70. 爬楼梯进阶版(完全背包)(opens new window)
如果求最小数,那么两层for循环的先后顺序就无所谓了,相关题目如下:
求最小数:动态规划:322. 零钱兑换 (opens new window)、动态规划:279.完全平方数(opens new window)
对于背包问题,其实递推公式算是容易的,难是难在遍历顺序上,如果把遍历顺序搞透,才算是真正理解了。
198.打家劫舍
思考该问题的求解思路: 当前偷或者不偷的状态受到哪些状态的影响,如何利用这些状态建立递推关系。当前状态和前面状态会有一种依赖关系,那么这种依赖关系都是动规的递推公式。
动归五步曲:
- dp数组含义(结合问题求的结果是什么): dp[i]考虑下标i则能够偷到的最大的钱币。
- 递推公式:偷i的话 dp[i-2]+coin[i],不偷i-1 dp[i-1],dp数组的含义表示i之前能够得到的最大金币数。
- 初始化dp数组:dp[0] = nums[0],dp[1] = max(nums[1],nums[0])
- 遍历顺序
- 打印dp数组
class Solution:
def rob(self, nums: List[int]) -> int:
'''
定义子问题:如何将原问题划分成一个一个的子问题,子问题就是对之前问题的解决,所以可以不用关注子问题,把注意力集中在此后问题的求解上
写出子问题的递推关系
确定 DP 数组的计算顺序'''
N = len(nums)
if N == 0:
return 0
if N == 1:
return sum(nums)
dp = [0]*(N+1)
dp[0] = 0
dp[1] = nums[0]
for k in range(2,N+1):
dp[k] = max(dp[k-1],dp[k-2]+nums[k-1])
return dp[N]
213 打家劫舍II
由于线性数组出现了首尾相接的形式,为了解决该问题将线性数组进行拆分即可。先考虑前四个,再考虑后四个值,这样的话就可以对原始的数组进行拆分,在求解两个部分的最大值,即得到整体的最大值。主要思想就是让首尾元素不会相互影响。
class Solution:
def traversal(self,nums):
N = len(nums)
if N == 0:
return 0
if N == 1:
return sum(nums)
dp = [0] * N
dp[0] = nums[0]
dp[1] = max(nums[0],nums[1])
for k in range(2,N):
dp[k] = max(dp[k-1],dp[k-2]+nums[k])
return dp[N-1]
def rob(self, nums: List[int]) -> int:
# 线性数组连成环,首尾相连,在这个环中偷的最大金币是多少。
if len(nums) == 1:
return nums[0]
n = len(nums)
left = self.traversal(nums[0:n-1])
right = self.traversal(nums[1:n])
return max(left,right)