给你一个字符串 s 和一个字符串列表 wordDict 作为字典。请你判断是否可以利用字典中出现的单词拼接出 s 。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
输入: s = “leetcode”, wordDict = [“leet”, “code”]
输出: true
解释: 返回 true 因为 “leetcode” 可以由 “leet” 和 “code” 拼接成。
动态规划是什么?
动态规划是用来求解最优化问题,将一个大问题划分成许多子问题,这些分解的子问题自身就是最优的才能得到最终问题的最优解。
可以用动态规划解决的问题一般有以下特点:
1.最优化远离: 问题的最优解所包含的子问题的解也是最优的,称该问题具有最优子结构,满足最优化原理。
2.无后效性: 某状态以后的过程不会影响该状态之前的过程。我的理解就是例如从前往后遍历时,dp[i+1]是什么状态并不会影响dp[i]或者dp[i-1]的状态。
3.重叠子结构: 子问题不是独立的,一个子问题在下一阶段的决策中可能被多次使用到。这个特点并不是必须的,但是如果没有这个特点,与其他方法相比,动态规划就没有更大的优势。
动态规划五要素,dp数组下标及其表示的含义,递推公式,初始化,遍历的顺序和实例推导。
这题中dp[i]表示的是s[:i]能否由wordDict里面的单词组合而成。dp[0]初始值为1,“”空串默认可以,因为后面的dp由dp[0]推导的,如果dp[0]是0,后面的就没法完了。
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n = len(s)
dp = [0] * (n+1)
dp[0] = 1
for i in range(1,n+1):
for j in range(n):
#这个代码的核心思想 dp[j]为1,而且s[j:i]在
# wordDict 那dp[i]肯定也为1
if dp[j] and s[j:i] in wordDict:
dp[i] = 1
return dp[-1] == 1
说到这个动态规划,就不得不复习一下背包问题。什么是背包问题?
物品有容量weights和对应的价值values,给一个一定容量的背包,求背包能装下的物品最大价值是多少。
0-1背包问题就是每个物品只能拿一次,完全背包问题是每个物品无穷多个,每次物品都可以拿很多次。
dp[i][j] 表示的是前i件物品放入容量为j 的背包中所得的物品的最大价值。
在每次遍历物品i的时候都有两种选择,拿或者不拿。
如果不拿的话,当前背包的价值和遍历上一个物品时一样,dp[i][j] = dp[i-1][j]。
如果拿的话,dp[i][j] = dp[i-1][j-weights[i]] + values[i]。怎么理解?前i-1件物品放入容量为j-weights[i]的最大价值加上当前物品的价值。为什么是j-weights[i],因为新加入的物品的重量是weights[i],而背包总容量是j,所以只能是前i-1的物品放在容量为j-weights[i]的背包中的最大值加上当前物品的价值。
综上,背包问题的动态规划公式为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]]+value[i])
那遍历顺序如何?
对于0-1背包问题而言遍历顺序有两种,一种是先遍历物品再遍历背包,一种是先遍历背包再遍历物品。如果是二维的dp,两个顺序都可以,但是一维的dp,只能是先物品再容量(为什么要先物品后容量?)。
先看二维dp的核心代码:
# 有n件物品,背包最大的容量是weight
# 初始化 行是n件物品 列是容量
dp = [[0]*(weight+1) for _ in range(n)]
# 当背包容量为0的时候,价值一定为0
# 对于第一件物品,当当前容量j<背包的总容量时,放不下嘛,所以为0
# j大于等于第一个物品的重量时,价值为第一件物品的价值。
for j in range(weight):
if j >= weights[i]:
dp[0][j] = values[i]
# 先遍历物品
for i in range(1,n):
# 再遍历容量
for j in range(1,weight+1):
if weights[i] > j:
dp[i][j] = dp[i-1][j]
else:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]]+values[i])
在看一维dp,一维dp的遍历顺序只能先物品再背包,并且背包是倒序。 为什么?
首先dp[j]表示的含义是容量为j的背包所得物品的最大价值是dp[j]。
背包倒序是为了让每个物品只被选入一次,如果是正序的话,同一个物品可能会被重复选入,但是从后往前遍历,每次取得状态不会和之前取得状态重合,保证了每个物品只取一次。
那二维dp的背包容量为什么不需要倒序呢?
因为二维dp[i][j]是通过dp[i-1][j]或者dp[i-1][j-weights[i]]+values[i]所得,是根据上一行的数据得到的,本行的数据不会被覆盖。
换一种说话,一位dp本质上还是二维dp,右下角的值依靠左上的值得到,所以要使得左上的值依然是上一层的,从右往左覆盖。
那为什么先物品再背包呢?
因为背包容量是要倒序的,如果背包放在外层,dp[j]中只会有一个物品。
def beibao_one(weight, weights, values):
n = len(weights)
dp = [0]*(weight+1)
# 先遍历物品 再遍历背包
for i in range(n):
# 背包容量从大到小遍历
for j in range(weight, weights[i]-1, -1):
dp[j] = max(dp[j], dp[j-weights[i]]+values[i])
print(dp)
此外说一下完全背包问题。完全背包和0-1背包的不同点就在于每个物品可以重复选,所以代码上的区别也很明显,背包容量从小到大遍历即可。 完全背包问题先遍历物品再遍历背包以及先遍历背包在遍历物品都可以。
这题的动态规划的代码如下:
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
dp = [0]*(len(s)+1)
#dp[0]表示空串 因为后面的推导都是从dp[0]出发 所以dp[0]为1
dp[0] = 1
#遍历背包 字符串s
for i in range(1,len(s)+1):
#遍历物品 字典中的单词
for j in range(i):
#如果dp[j]为真且[j,i]子串在字典中则dp[i]为1
if dp[j]==1 and s[j:i] in wordDict:
dp[i] = 1
# print(dp)
return dp[-1] == 1
首先什么是记忆化搜索?
自己的理解就是把计算过的结果保存下来,当下次需要这个值的时候就不用重复计算而是直接找到之前计算过并保存下来的结果。核心就是避免了重复计算。顾名思义,记忆化搜索,之前有了这么一个记忆(结果),直接调出来使用而不是再次计算。是用空间换时间的一种做法。
搜索的低效是因为没有很好的处理重叠子问题,动态规划虽然较好地处理了重叠子问题,但是对于拓扑结构比较复杂的问题又显得无奈,记忆化搜索针对这种问题而提出,它结合了搜索的形式和动态规划中递归的思想。
代码:
import functools
@functools.lru_cache(None)
def back_track(s):
if not s:
return True
res=False
for i in range(1,len(s)+1):
if(s[:i] in wordDict):
res=back_track(s[i:]) or res
return res
back_track(s)
关于Python中的@cache和@lru_cache
这两个我在刷题的过程中都遇见过。这是python提供的两种不同的缓存装饰器。
其中@lru_cache是模块functools中的,lru是least recently used。最近最少被使用,当缓存队列满了之后,就会把最近最少使用的结果删除。
有两个参数,@lru_cache(maxsize=128, typed=False)。前者表示缓存区的大小,默认是128,如果是None的话表示可以存无限个。typed=True时不同参数类型的调用将分别缓存,默认False。
其中@cache是内置模块 functools 提供的高阶函数 @functools.cache,它是简单轻量级无长度限制的函数缓存。它是 3.9 新版功能,是在 lru_cache 缓存基础上简化了的对无限长度缓存。
语法为 @functools.cache(user_function),创建一个查找函数参数的字典的简单包装器。 因为它不需要移出旧值,缓存大小没有限制,所以比带有大小限制的 lru_cache() 更小更快。这个 @cache 装饰器是 Python 3.9 版中的新功能,在此之前,可以通过 @lru_cache(maxsize=None) 获得相同的效果。
这题我今天是刷第五遍,但“有趣”的是,我一点思路都没有。看了题目的关键词,记忆化搜索、回溯、动态规划,但是依然是没有思路。
很是沮丧。看到之前提交的代码就那么一小点,心碎的稀烂。
单词拆分,好样的。