题目:给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,确定 s 是否可以被空格分割为一个或多个在字典里出现的单词。你可以假设字典中无重复的单词。
例如,给出
s = "leetcode"
,
dict = ["leet", "code"]
。
返回 true 因为 "leetcode"
可以被切分成 "leet code"
。
分析:
字符串每一个字符位置后都有两种选择,插or不插,类似于一个背包问题,考虑使用动态规划,划分为多个阶段每个阶段有插与不插两种选择。
动态规划的方法论:
动态规划的关键是推导出递推公式,有时候直接写出递推公式是非常困难的,在《挑战程序设计》一书中给出了一种思考dp问题的通用方法(写本篇文章的主要目的是为了介绍此书提出的关于动态规划的思考方法)。
首先,使用递归,以递归的思路思考递推关系更容易;
其次,分析递归,可以展开递归,分析递归的重复计算,将重复计算存储起来,优化递归;
最后,分析递归+记忆数组,从记忆数组和递归关系中找出递推关系。
解题:
1.递归
class Solution(object):
def wordBreak(self, s, wordDict):
"""
:type s: str
:type wordDict: List[str]
:rtype: bool
"""
return self._wordBreak(s,wordDict,0,-1)
def _wordBreak(self,s,wordDict,i,lastSplitPos):
if i>=len(s)-1:
return s[lastSplitPos+1:i+1] in wordDict
else:
#若i位置和上一次插入位置不构成单词则不能插入
if s[lastSplitPos+1:i+1] not in wordDict:
return self._wordBreak(s,wordDict,i+1,lastSplitPos)
else:
return self._wordBreak(s,wordDict,i+1,i) or self._wordBreak(s,wordDict,i+1,lastSplitPos)
i代表阶段,决策是否给s[i]后插入空格。lastSplitPos代表上一次插入空格位置。因此,若s[lastSplitPos+1:i+1] not in wordDict,则第i阶段不能插入空格。否则,则有两种选择,插入空格则lastSplitPos=i,不插则lastSplitPos.以上代码提交leetcode超时,下一步分析重复。
2.记忆
class Solution(object):
def wordBreak(self, s, wordDict):
"""
:type s: str
:type wordDict: List[str]
:rtype: bool
"""
dp=[-1]*len(s)
if s in wordDict:
dp[0]=True
return self._wordBreak(s,wordDict,0,-1,dp)
def _wordBreak(self,s,wordDict,i,lastSplitPos,dp):
#分析递归找重复
if dp[lastSplitPos+1]!=-1:
return dp[lastSplitPos+1]
if i>=len(s)-1:
return s[lastSplitPos+1:i+1] in wordDict
else:
#若s[lastSplitPos+1:i+1] not in wordDict则此位置不能插入空格
if s[lastSplitPos+1:i+1] not in wordDict:
dp[lastSplitPos+1]=self._wordBreak(s,wordDict,i+1,lastSplitPos,dp)
return dp[lastSplitPos+1]
else:
dp[i+1]=self._wordBreak(s,wordDict,i+1,i,dp)
dp[lastSplitPos+1]=self._wordBreak(s,wordDict,i+1,lastSplitPos,dp)
return dp[i+1] or dp[lastSplitPos+1]
假设输入字符串为"leetcode",展开递归分析
如图,左边代表此分支切割,右边代表此分支不切割,红圈位置出现了重复计算。因此使用dp存储重复计算,dp[lastSplitPos],代表从lastSplitPos位置切分后,s[lastSplitPos+1:len(s)]串是否存在切分满足题目要求,提交代码,通过。
3.递推公式
if s[lastSplitPos+1:i+1] not in wordDict:
dp[i][lastSplitPos]= dp[i+1][lastSplitPos]
else:
dp[i][lastSplitPos]= dp[i+1][lastSplitPos] or dp[i+1][i]
通过递归+记忆我们发现重点变量有两个,i和lastSplitPos。i代表了第i个阶段,lastSplitPos是上一次切分位置。
dp[i][lastSplitPos]代表了第i个阶段,lastSplitPos上一次切分位置,s[lastSplitPos+1:len(s)]是否满足题目要求.
0->1->2->i->i+1->...
以输入leet,字典为l,e,et为例,依赖从0-3阶段,递推从3-0阶段最终dp[0][-1]为答案,注意数组下标没有-1,这里是为了方便和递归比较.
#-------------------------------------------------------------------------------------------------------------#
补充:
写完博客发表后,看到了一个关联的博客 https://blog.youkuaiyun.com/congduan/article/details/45245975,给出的解法更简单,这个解法的思路使用的是字符串问题最常用的DP思路,即从子串递推到全部串,用一个数组记录,dp[i]表示从s[0:i]是否满足题目要求,一直递推到dp[len(s)].
总结:
当不能直接写出动态规划的递推式时,可以采用递归+记忆数组的方式。
字符串dp的一般思路是从解决子s[0:i]问题,递推到解决s[0:i+1]问题
动态规划解决的是重叠子问题,即解决问题A需要通过子问题B来解决,而解决B要通过解决B的子问题C来解决,一环套一环。重叠子问题会出现重复计算,因此存储以前的计算结果.