问题描述:
给定一个非空字符串 s 和一个包含非空单词列表的字典 wordDict,判定 s 是否可以被空格拆分为一个或多个在字典中出现的单词。
拆分时可以重复使用字典中的单词。
可以假设字典中没有重复的单词。
示例 1:
输入: s = "leetcode", wordDict = ["leet", "code"]
输出: true
解释: 返回 true 因为 "leetcode" 可以被拆分成 "leet code"。
来源:力扣(LeetCode)
链接:https://leetcode-cn.com/problems/word-break
著作权归领扣网络所有。商业转载请联系官方授权,非商业转载请注明出处。
解法一:递归
问题可以转化为以当前结点为开头的序列能否由字典表示,以0位置开头即为所求。遍历过程为,若从开头到当前的结点的子串在字典中,之后只需判断从当前结点后面开头的序列能否由字典表示;若可以 则以开头位置的字符串全部能由字典表示。实现代码如下:
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
return process(s, 0, new HashSet<>(wordDict));
}
public boolean process(String s, int start, Set<String> set){
if(start == s.length()){
return true;
}
for(int i = start + 1; i <= s.length(); i++){
if(set.contains(s.substring(start, i)) && process(s, i, set)){
return true;
}
}
return false;
}
}
解法二:带有记忆的递归
发现解法一中进行了大量的重复计算,因此很容易想到使用一个记忆存储计算结果。之后先进行判断,若存在则直接读取,不存在就进行同解法一那样计算。实现代码如下:
class Solution {
public boolean wordBreak(String s, List<String> wordDict) {
Boolean[] memo = new Boolean[s.length()]; // memo[i] 为以i开始能否拆成字典
return process(s, 0, new HashSet<>(wordDict), memo);
}
public boolean process(String s, int start, Set<String> set, Boolean[] memo){
if(start == s.length()){
return true;
}
if(memo[start] != null){
return memo[start];
}
for(int i = start + 1; i <= s.length(); i++){
if(set.contains(s.substring(start, i)) && process(s, i, set, memo)){
memo[start] = true;
return true;
}
}
memo[start] = false;
return false;
}
}
解法三:动态规划
dp算法的一般优化顺序为递归 => 递归+记忆集 => 动态规划
我们发现前面的结点求解只依赖后续结点的信息,可以从后往前依次计算结点的后缀能否由字典组成。此外由于我们只关心能由字典组成的后缀,可以使用list代替dp数组,list中只存储为true的下标。实现代码如下:
public boolean wordBreak(String s, List<String> wordDict) {
List<Integer> list = new ArrayList<>(); // 以list[i]作为开头可以由字典表示
list.add(s.length());
Set<String> set = new HashSet<>(wordDict);
for(int i = s.length() - 1; i >= 0; i--){
for(int end : list){
if(set.contains(s.substring(i, end))){
list.add(i);
break;
}
}
}
// System.out.println(list);
if(list.isEmpty() || list.get(list.size() - 1) != 0){
return false;
}
return true;
}