理解与分析
首先介绍一下这四者的区别和联系:剑指offer2 类型总结:循环,递归,迭代和动态规划
递推和回溯:是递归的两个阶段!然后记忆化,剪枝都是为了更快(或更低复杂度)完成的操作
单词拆分
- dfs V1.0(这一版会超出时间限制,所以我们需要+记忆化):
class Solution:
flag = False
def wordBreak(self, s: str, wordDict) -> bool:
@cache
def dfs(s: str) -> bool:
global flag
# 空字符串不是None,None是空字符串
if s == "":
flag = True
else:
flag = False
# 注意range和字符串边界问题
for _ in range(len(s)):
if s[0:_+1] in wordDict:
dfs(s[_+1:])
if flag:
break
# 注意递归的特性,此处才是最终返回的,也就是只有最外层的才会被返回,所以一般是在递归函数之外定义global(或成员)变量/直接使用全局变量如列表等
return flag
return dfs(s)
- dfs V2.0:
-
python简直是黑科技…其实只需要在dfs上面加上一个
@cache
,相当于机器帮你完成了记忆化的操作,但其实这个记忆化非常简单,是对函数的参数和结果进行了一个存储。瞬间从超时变成了击败97%的用户,第一次用感觉太神奇了,记录一下:
-
下面这是正儿八经的记忆化操作:还是先找状态转移方程,如果第一遍分割的dfs未通过,在进行第二遍分割时候,s的前半部分在第一次判断过了,那再次遍历的时候只需要在维护的列表中取出来结果+判断后面的就好啦,所以
true if s[0:_] and s[_:]
# debug不出来了,天啊😭还是超时,好像没起到记忆化的效果 class Solution: flag = False def wordBreak(self, s: str, wordDict) -> bool: memo = [False for _ in range(len(s))] def dfs(start: int, s: str) -> bool: global flag # 空字符串不是None,None是空字符串 if s == "": flag = True else: flag = False # 注意range和字符串边界问题 for _ in range(len(s)): print(start+_, memo) if memo[start+_] or s[0:_+1] in wordDict: memo[start+_] = True dfs(start+_+1, s[_+1:]) if flag: memo[start+_] = True break return flag return dfs(0, s)
-
- 动态规划:所有的递归的返回过程实际上就是动态规划!!!让我们从开始条件(逐步分解至最小的情况即第一个词就是单词)记录,直到最终填充完所有的memo(下面代码中使用的是dp),这是力扣一个解答中的代码,包括上面的dfs也有更好的写法,太优雅啦:
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> bool:
n=len(s)
dp=[False]*(n+1)
dp[0]=True
for i in range(n):
for j in range(i+1,n+1):
if(dp[i] and (s[i:j] in wordDict)):
dp[j]=True
return dp[-1]
单词拆分Ⅱ
- dfs+回溯过程中的记忆化(防止超时),将list转为Set(哈希存取),能进一步节省资源:
class Solution:
# 带备忘录的记忆化搜索,使用@cache是一个道理
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
res = []
memo = [1] * (len(s)+1)
wordDict = set(wordDict)
def dfs(wordDict, ans, pos):
# 回溯前先记下答案中有多少个元素
num = len(res)
if pos == len(s):
res.append(" ".join(ans))
return
for i in range(pos, len(s)+1):
if memo[i] and s[pos:i] in wordDict:
ans.append(s[pos:i])
dfs(wordDict, temp, i)
temp.pop()
# 答案中的元素没有增加,说明s[pos:]不能分割,修改备忘录
memo[pos] = 1 if len(res) > num else 0
dfs(wordDict, [], 0)
return res
- 先动态规划判断是否能(动态规划的利弊就看出来啦,因为是串,用一个数组记录,将二维->一维,同时不再有记录路径过程,主要是为了防止 不能分割的长字符串的不断递归 ),再获得路径:
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
n = len(s)
# 1.动态规划判断单词是否可拆分,其实这里就是针对那个"aaaaaaaaa"的情况,让它不要通过Dfs的算法去递归遍历了
dp = [False]*(n+1)
dp[0] = True
for i in range(n):
for j in range(i+1, n+1):
if s[i:j] in wordDict and dp[i]:
dp[j] = True
if not dp[-1]:
return []
# 2.对于可拆分的数组,使用DFS求得所有拆分组合:即简化问题为“给你一个可拆分的数组,将可拆分的组合返回”,就很经典了
result = []
def dfs(string, ans):
if len(string)<=0:
result.append(" ".join(ans[:]))
return
for i in range(0, len(string)+1):
if string[0:i] in wordDict:
ans.append(string[0:i])
dfs(string[i:], ans)
ans.pop()
dfs(s, [])
return result
- 根据题意,因为worddict个数是有限的,所以直接根据它的长度来分:
class Solution:
def wordBreak(self, s: str, wordDict: List[str]) -> List[str]:
def dfs(count):
if count == 0:
res.append(' '.join(lst))
return
for word in wordDict:
size = len(word)
if size <= count and s[N-count:N-count+size] == word:
lst.append(word)
dfs(count - size)
lst.pop()
res = []
lst = []
N = len(s)
dfs(N)
return res