题目描述
给你一个字符串 s 和一个字符串列表 wordDict 作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s 则返回 true。
注意:不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用。
示例 1:
输入:
s = "leetcode",wordDict = ["leet", "code"]
输出:true
解释:返回true因为leetcode可以由leet和code拼接成。
示例 2:
输入:
s = "applepenapple",wordDict = ["apple", "pen"]
输出:true
解释:返回true因为applepenapple可以由apple、pen和apple拼接成。
示例 3:
输入:
s = "catsandog",wordDict = ["cats", "dog", "sand", "and", "cat"]
输出:false
提示:
1 <= s.length <= 3001 <= wordDict.length <= 10001 <= wordDict[i].length <= 20s和wordDict[i]仅由小写英文字母组成wordDict中的所有字符串互不相同
动态规划
我们可以使用动态规划来解决这个问题。定义 dp[i] 表示字符串 s 的前 i 个字符(即子串 s[0:i-1])是否可以被拆分成字典中的单词。
算法步骤
- 将单词字典转换为哈希集合,提高查找效率
- 初始化
dp[0] = true,表示空字符串可以被拆分 - 对于每个位置
i(从1到字符串长度),检查所有可能的分割点j(从0到i-1) - 如果
dp[j]为真且子串s[j:i]在字典中,则设置dp[i] = true - 最终
dp[s.length()]就是整个字符串是否可以被拆分的答案
代码实现
1 | public boolean wordBreak(String s, List<String> wordDict) { |
动态规划过程详解
以 s = "leetcode", wordDict = ["leet", "code"] 为例,详细说明动态规划每一步的计算过程:
- 初始化:
dp[0] = true(空字符串可以被拆分)。 - i=1:检查前 1 个字符 “l”。
- j=0:子串 “l” 不在字典中,
dp[1] = false。
- j=0:子串 “l” 不在字典中,
- i=2:检查前 2 个字符 “le”。
- j=0:子串 “le” 不在字典中。
- j=1:子串 “e” 不在字典中,
dp[2] = false。
- i=3:检查前 3 个字符 “lee”。
- j=0:子串 “lee” 不在字典中。
- j=1:子串 “ee” 不在字典中。
- j=2:子串 “e” 不在字典中,
dp[3] = false。
- i=4:检查前 4 个字符 “leet”。
- j=0:子串 “leet” 在字典中,且
dp[0]=true,所以dp[4] = true(跳出内层循环)。
- j=0:子串 “leet” 在字典中,且
- i=5:检查前 5 个字符 “leetc”。
- j=0:子串 “leetc” 不在字典中。
- j=1:子串 “eetc” 不在字典中。
- j=2:子串 “etc” 不在字典中。
- j=3:子串 “tc” 不在字典中。
- j=4:子串 “c” 不在字典中(注意:这里检查的是从索引 4 到 5 的子串 “c”,因为
s.substring(4,5)返回 “c”),所以dp[5] = false。
- i=6:检查前 6 个字符 “leetco”。
- 类似地,检查所有分割点,没有找到满足条件的子串,
dp[6] = false。
- 类似地,检查所有分割点,没有找到满足条件的子串,
- i=7:检查前 7 个字符 “leetcod”。
- 没有满足条件的子串,
dp[7] = false。
- 没有满足条件的子串,
- i=8:检查前 8 个字符 “leetcode”。
- j=0:子串 “leetcode” 不在字典中。
- j=1:子串 “eetcode” 不在字典中。
- … 其他分割点也不满足。
- j=4:子串 “code” 在字典中,且
dp[4]=true,所以dp[8] = true。
- 最终返回
dp[8]=true,表示 “leetcode” 可以被拆分为 “leet” 和 “code”。
复杂度分析
- 时间复杂度:
O(n²),其中n是字符串的长度。因为有两层循环,外层循环遍历每个字符,内层循环最多遍历i次 - 空间复杂度:
O(n + m),其中n是字符串长度(dp数组大小),m是字典中的单词数量(哈希集合大小)
剪枝优化
为了提升性能,我们引入两种剪枝优化:
- 最大长度剪枝:
- 通过计算字典中单词的最大长度,我们避免了检查那些长度超过最大单词长度的子串,这可以显著减少内层循环的次数。
- 例如,当检查位置i=5时,我们只需要从j=1开始检查,而不是从j=0开始。
- 从后向前检查:
- 通过从后向前检查分割点,我们优先检查较短的子串,这样一旦找到匹配的单词就可以提前退出循环,减少不必要的检查。
- 在实际应用中,较短的单词通常更常见,因此这种检查顺序可以提高效率。
代码实现
1 | public boolean wordBreak(String s, List<String> wordDict) { |
总结
单词拆分问题是一个经典的动态规划问题,通过合理的剪枝优化,我们可以显著提高算法的性能。本文介绍了两种有效的优化技巧:长度剪枝和检查顺序优化,并通过代码示例展示了如何实现这些优化。在实际应用中,根据具体情况选择合适的优化策略,可以在保证正确性的同时提高算法的执行效率。
来源
139. 单词拆分 | 力扣(LeetCode)
139. 单词拆分 | 题解 | 力扣官方题解
139. 单词拆分 | 题解 | 灵茶山艾府