139.单词拆分
题目
给你一个字符串 s
和一个字符串列表 wordDict
作为字典。如果可以利用字典中出现的一个或多个单词拼接出 s
则返回 true
。
**注意:**不要求字典中出现的单词全部都使用,并且字典中的单词可以重复使用
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以由 "leet" 和 "code" 拼接成。
解题思路
-
动态规划 :使用动态规划的思想,从左到右遍历字符串
s
,判断每个前缀是否可由wordDict
中的单词拼接而成。 -
状态定义 :定义一个布尔数组
canBreak[i]
表示字符串s
的前i
个字符是否可拼接。 -
初始化 :
canBreak[0]
初始化为true
,表示空字符串始终可拼接。 -
状态转移 :对于每个索引
i
(1 到s.length()
),检查s
从索引0
到i-1
的每个可能前缀是否在字典中,并且前缀的结尾字符与当前索引i
处的字符相匹配。
解题过程
-
初始化一个长度为
s.length() + 1
的布尔数组canBreak
,并设置canBreak[0]
为true
。 -
使用两层循环:外层循环遍历字符串
s
的每个索引i
。内层循环从0
遍历到i-1
,尝试找到所有可能的前缀。 -
在内层循环中,如果
canBreak[j]
为true
且s
从索引j
到i
的子字符串在字典wordDict
中,则设置canBreak[i]
为true
并跳出内层循环。 -
继续外层循环直到遍历完所有索引。
-
最后,返回
canBreak[s.length()]
,表示整个字符串s
是否可拼接。
复杂度
-
时间复杂度 :最坏情况下,算法需要检查字符串
s
的所有子字符串是否在字典wordDict
中。对于长度为n
的字符串,有O(n^2)
个子字符串,每个子字符串的查找操作的时间复杂度为O(m)
(其中m
是字典wordDict
的大小)。因此,总时间复杂度为O(n^2 * m)
。 -
空间复杂度 :使用了一个长度为
n + 1
的布尔数组来存储中间结果,空间复杂度为O(n)
。
Code
java
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
boolean[] canBreak = new boolean[s.length() + 1];
canBreak[0] = true;
for (int i = 1; i <= s.length(); i++) {
for (int j = 0; j < i; j++) {
if (canBreak[j] && wordDict.contains(s.substring(j, i))) {
canBreak[i] = true;
break;
}
}
}
return canBreak[s.length()];
}
}
140.单词拆分II
题目
给定一个字符串 s
和一个字符串字典 wordDict
,在字符串 s
中增加空格来构建一个句子,使得句子中所有的单词都在词典中。以任意顺序 返回所有这些可能的句子。
**注意:**词典中的同一个单词可能在分段中被重复使用多次。
示例 1:
输入: s = "catsanddog", wordDict = ["cat","cats","and","sand","dog"]
输出:["cats and dog","cat sand dog"]
解题思路
问题定义 :给定一个字符串 s
和一个字符串字典 wordDict
,任务是在字符串 s
中增加空格来构建一个句子,使得句子中所有的单词都在字典 wordDict
中。这个问题可以通过回溯算法来解决。回溯算法是一种通过探索所有可能的候选解来找出所有解决方案的方法。为了避免重复计算相同子问题的解,我们使用记忆化技术来存储已经解决的子问题的解。
解题过程
-
初始化 :创建一个
memo
哈希表来存储已经计算过的子问题的解。 -
递归函数 :实现一个
backtrack
函数,该函数尝试在字符串s
的不同位置添加空格,以找到所有可能的分割方式。 -
终止条件 :如果当前索引
start
等于字符串s
的长度,返回一个只包含空字符串的列表,表示已经到达字符串末尾。 -
记忆化 :在
backtrack
函数中,首先检查memo
是否已经包含了当前start
索引的解,如果包含,则直接返回该解。 -
回溯搜索 :遍历字符串
s
从当前索引start
到字符串末尾的每个位置end
,检查s
从start
到end
的子字符串是否在字典wordDict
中。 -
递归调用 :对于每个有效的子字符串,递归调用
backtrack
函数,使用end
作为新的起始索引。 -
结果合并:将当前有效的子字符串与递归调用的结果合并,构建完整的句子。
-
存储结果 :将当前
start
索引的所有可能解存储在memo
中,以便后续使用。
复杂度
-
时间复杂度 :最坏情况下,算法需要遍历字符串
s
的所有可能的子字符串。对于长度为n
的字符串,有O(n^2)
个子字符串。对于每个子字符串,我们需要 O(k) 的时间来检查它是否存在于字典中,其中 k 是字典中单词的平均长度。因此,最坏情况下的时间复杂度是 O(n^2 * k)。然而,由于记忆化减少了重复计算,实际的时间复杂度可能会更低。 -
空间复杂度 :空间复杂度主要由存储子问题解的
memo
哈希表决定。在最坏的情况下,我们可能需要存储每个子问题的解,这将需要 O(n * k) 的空间。此外,递归调用栈的空间复杂度也是 O(n)。
Code
java
class Solution {
public List<String> wordBreak(String s, List<String> wordDict) {
Set<String> wordSet = new HashSet<>(wordDict);
Map<Integer, List<String>> memo = new HashMap<>();
return backtrack(s, 0, wordSet, memo);
}
private List<String> backtrack(String s, int start, Set<String> wordSet, Map<Integer, List<String>> memo) {
if (start == s.length()) {
return new ArrayList<>(Collections.singletonList("")); // 返回包含空字符串的列表
}
// 如果结果已经被计算过,直接从记忆化存储中获取结果
if (memo.containsKey(start)) {
return memo.get(start);
}
List<String> result = new ArrayList<>();
for (int end = start + 1; end <= s.length(); end++) {
String word = s.substring(start, end);
if (wordSet.contains(word)) {
List<String> subResults = backtrack(s, end, wordSet, memo);
for (String subResult : subResults) {
result.add(word + (subResult.isEmpty() ? "" : " " + subResult));
}
}
}
// 将计算结果存储在记忆化存储中
memo.put(start, result);
return result;
}
}